blob: 326192072c4bcc05f8c4d22bdc62662b5915ae4f [file] [log] [blame]
/*
* Copyright 2000-2014 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;
import com.intellij.ide.ui.UISettings;
import com.intellij.ide.ui.UISettingsListener;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.*;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.wm.*;
import com.intellij.ui.*;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.ui.awt.RelativeRectangle;
import com.intellij.ui.components.OrphanGuardian;
import com.intellij.ui.switcher.QuickActionProvider;
import com.intellij.ui.switcher.SwitchProvider;
import com.intellij.ui.switcher.SwitchTarget;
import com.intellij.ui.tabs.*;
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.util.Consumer;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.Animator;
import com.intellij.util.ui.JBInsets;
import com.intellij.util.ui.TimedDeadzone;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.update.ComparableObject;
import com.intellij.util.ui.update.LazyUiDisposable;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.plaf.ComponentUI;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Line2D;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.*;
import java.util.List;
public class JBTabsImpl extends JComponent
implements JBTabs, PropertyChangeListener, TimerListener, DataProvider, PopupMenuListener, Disposable, JBTabsPresentation, Queryable,
QuickActionProvider {
public static final DataKey<JBTabsImpl> NAVIGATION_ACTIONS_KEY = DataKey.create("JBTabs");
public static final Color MAC_AQUA_BG_COLOR = Gray._200;
@NotNull final ActionManager myActionManager;
private final List<TabInfo> myVisibleInfos = new ArrayList<TabInfo>();
private final Map<TabInfo, Integer> myHiddenInfos = new HashMap<TabInfo, Integer>();
private TabInfo mySelectedInfo;
public final Map<TabInfo, TabLabel> myInfo2Label = new HashMap<TabInfo, TabLabel>();
public final Map<TabInfo, Toolbar> myInfo2Toolbar = new HashMap<TabInfo, Toolbar>();
public Dimension myHeaderFitSize;
private Insets myInnerInsets = JBInsets.NONE;
private final List<EventListener> myTabMouseListeners = ContainerUtil.createLockFreeCopyOnWriteList();
private final List<TabsListener> myTabListeners = ContainerUtil.createLockFreeCopyOnWriteList();
private boolean myFocused;
private Getter<ActionGroup> myPopupGroup;
private String myPopupPlace;
TabInfo myPopupInfo;
final DefaultActionGroup myNavigationActions;
final PopupMenuListener myPopupListener;
JPopupMenu myActivePopup;
public boolean myHorizontalSide = true;
private boolean myStealthTabMode = false;
private boolean mySideComponentOnTabs = true;
private DataProvider myDataProvider;
private final WeakHashMap<Component, Component> myDeferredToRemove = new WeakHashMap<Component, Component>();
private final SingleRowLayout mySingleRowLayout;
private final TableLayout myTableLayout = new TableLayout(this);
private TabLayout myLayout;
private LayoutPassInfo myLastLayoutPass;
private TabInfo myLastPaintedSelection;
public boolean myForcedRelayout;
private UiDecorator myUiDecorator;
static final UiDecorator ourDefaultDecorator = new DefaultDecorator();
private boolean myPaintFocus;
private boolean myHideTabs = false;
@Nullable private Project myProject;
private boolean myRequestFocusOnLastFocusedComponent = false;
private boolean myListenerAdded;
final Set<TabInfo> myAttractions = new HashSet<TabInfo>();
private final Animator myAnimator;
private List<TabInfo> myAllTabs;
private boolean myPaintBlocked;
private BufferedImage myImage;
private IdeFocusManager myFocusManager;
private final boolean myAdjustBorders = true;
boolean myAddNavigationGroup = true;
private boolean myGhostsAlwaysVisible = false;
private boolean myDisposed;
private boolean myToDrawBorderIfTabsHidden = true;
private Color myActiveTabFillIn;
private boolean myTabLabelActionsAutoHide;
private final TabActionsAutoHideListener myTabActionsAutoHideListener = new TabActionsAutoHideListener();
private IdeGlassPane myGlassPane;
@NonNls private static final String LAYOUT_DONE = "Layout.done";
@NonNls public static final String STRETCHED_BY_WIDTH = "Layout.stretchedByWidth";
private TimedDeadzone.Length myTabActionsMouseDeadzone = TimedDeadzone.DEFAULT;
private long myRemoveDeferredRequest;
private boolean myTestMode;
private JBTabsPosition myPosition = JBTabsPosition.top;
private final TabsBorder myBorder = new TabsBorder(this);
private final BaseNavigationAction myNextAction;
private final BaseNavigationAction myPrevAction;
private boolean myTabDraggingEnabled;
private DragHelper myDragHelper;
private boolean myNavigationActionsEnabled = true;
private boolean myUseBufferedPaint = true;
private boolean myOwnSwitchProvider = true;
private SwitchProvider mySwitchDelegate;
protected TabInfo myDropInfo;
private int myDropInfoIndex;
protected boolean myShowDropLocation = true;
private TabInfo myOldSelection;
private SelectionChangeHandler mySelectionChangeHandler;
private Runnable myDeferredFocusRequest;
private boolean myAlwaysPaintSelectedTab;
private int myFirstTabOffset;
public JBTabsImpl(@NotNull Project project) {
this(project, project);
}
private JBTabsImpl(@NotNull Project project, @NotNull Disposable parent) {
this(project, ActionManager.getInstance(), IdeFocusManager.getInstance(project), parent);
}
public JBTabsImpl(@Nullable Project project, IdeFocusManager focusManager, @NotNull Disposable parent) {
this(project, ActionManager.getInstance(), focusManager, parent);
}
public JBTabsImpl(@Nullable Project project, @NotNull ActionManager actionManager, IdeFocusManager focusManager, @NotNull Disposable parent) {
myProject = project;
myActionManager = actionManager;
myFocusManager = focusManager != null ? focusManager : IdeFocusManager.getGlobalInstance();
setOpaque(true);
setPaintBorder(-1, -1, -1, -1);
Disposer.register(parent, this);
myNavigationActions = new DefaultActionGroup();
myNextAction = new SelectNextAction(this, myActionManager);
myPrevAction = new SelectPreviousAction(this, myActionManager);
myNavigationActions.add(myNextAction);
myNavigationActions.add(myPrevAction);
setUiDecorator(null);
mySingleRowLayout = createSingleRowLayout();
myLayout = mySingleRowLayout;
myPopupListener = new PopupMenuListener() {
@Override
public void popupMenuWillBecomeVisible(final PopupMenuEvent e) {
}
@Override
public void popupMenuWillBecomeInvisible(final PopupMenuEvent e) {
disposePopupListener();
}
@Override
public void popupMenuCanceled(final PopupMenuEvent e) {
disposePopupListener();
}
};
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(final MouseEvent e) {
if (mySingleRowLayout.myLastSingRowLayout != null &&
mySingleRowLayout.myLastSingRowLayout.moreRect != null &&
mySingleRowLayout.myLastSingRowLayout.moreRect.contains(e.getPoint())) {
showMorePopup(e);
}
}
});
addMouseWheelListener(new MouseWheelListener() {
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
if (mySingleRowLayout.myLastSingRowLayout != null) {
mySingleRowLayout.scroll(e.getUnitsToScroll() * mySingleRowLayout.getScrollUnitIncrement());
revalidateAndRepaint(false);
}
}
});
UISettings.getInstance().addUISettingsListener(new UISettingsListener() {
@Override
public void uiSettingsChanged(UISettings source) {
myImage = null;
for (Map.Entry<TabInfo, TabLabel> entry : myInfo2Label.entrySet()) {
entry.getKey().revalidate();
entry.getValue().setInactiveStateImage(null);
}
}
}, this);
myAnimator = new Animator("JBTabs Attractions", 2, 500, true) {
@Override
public void paintNow(final int frame, final int totalFrames, final int cycle) {
repaintAttractions();
}
};
setFocusCycleRoot(true);
setFocusTraversalPolicy(new LayoutFocusTraversalPolicy() {
@Override
public Component getDefaultComponent(final Container aContainer) {
return getToFocus();
}
});
add(mySingleRowLayout.myLeftGhost);
add(mySingleRowLayout.myRightGhost);
new LazyUiDisposable<JBTabsImpl>(parent, this, this) {
@Override
protected void initialize(@NotNull Disposable parent, @NotNull JBTabsImpl child, @Nullable Project project) {
if (project != null) {
myProject = project;
}
Disposer.register(child, myAnimator);
Disposer.register(child, new Disposable() {
@Override
public void dispose() {
removeTimerUpdate();
}
});
if (!myTestMode) {
final IdeGlassPane gp = IdeGlassPaneUtil.find(child);
if (gp != null) {
gp.addMouseMotionPreprocessor(myTabActionsAutoHideListener, child);
myGlassPane = gp;
}
UIUtil.addAwtListener(new AWTEventListener() {
@Override
public void eventDispatched(final AWTEvent event) {
if (mySingleRowLayout.myMorePopup != null) return;
processFocusChange();
}
}, AWTEvent.FOCUS_EVENT_MASK, child);
myDragHelper = new DragHelper(child);
myDragHelper.start();
}
if (myProject != null && myFocusManager == IdeFocusManager.getGlobalInstance()) {
myFocusManager = IdeFocusManager.getInstance(myProject);
}
}
};
putClientProperty(OrphanGuardian.CLIENT_PROPERTY_KEY, new OrphanGuardian() {
@Override
public void iterateOrphans(Consumer<JComponent> consumer) {
for (TabInfo info : getVisibleInfos()) {
if (info == mySelectedInfo) continue;
consumer.consume(info.getComponent());
}
}
});
}
protected SingleRowLayout createSingleRowLayout() {
return new SingleRowLayout(this);
}
@Override
public JBTabs setNavigationActionBinding(String prevActionId, String nextActionId) {
if (myNextAction != null) {
myNextAction.reconnect(nextActionId);
}
if (myPrevAction != null) {
myPrevAction.reconnect(prevActionId);
}
return this;
}
public boolean isEditorTabs() {
return false;
}
@Override
public JBTabs setNavigationActionsEnabled(boolean enabled) {
myNavigationActionsEnabled = enabled;
return this;
}
@Override
public final boolean isDisposed() {
return myDisposed;
}
@Override
public JBTabs setAdditionalSwitchProviderWhenOriginal(SwitchProvider delegate) {
mySwitchDelegate = delegate;
return this;
}
public static Image getComponentImage(TabInfo info) {
JComponent cmp = info.getComponent();
BufferedImage img;
if (cmp.isShowing()) {
final int width = cmp.getWidth();
final int height = cmp.getHeight();
img = UIUtil.createImage(width > 0 ? width : 500, height > 0 ? height : 500, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = img.createGraphics();
cmp.paint(g);
}
else {
img = UIUtil.createImage(500, 500, BufferedImage.TYPE_INT_ARGB);
}
return img;
}
@Override
public void dispose() {
myDisposed = true;
mySelectedInfo = null;
resetTabsCache();
myAttractions.clear();
myVisibleInfos.clear();
myUiDecorator = null;
myImage = null;
myActivePopup = null;
myInfo2Label.clear();
myInfo2Toolbar.clear();
myTabListeners.clear();
}
void resetTabsCache() {
myAllTabs = null;
}
private void processFocusChange() {
Component owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
if (owner == null) {
setFocused(false);
return;
}
if (owner == this || SwingUtilities.isDescendingFrom(owner, this)) {
setFocused(true);
}
else {
setFocused(false);
}
}
private void repaintAttractions() {
boolean needsUpdate = false;
for (TabInfo each : myVisibleInfos) {
TabLabel eachLabel = myInfo2Label.get(each);
needsUpdate |= eachLabel.repaintAttraction();
}
if (needsUpdate) {
relayout(true, false);
}
}
@Override
public void addNotify() {
super.addNotify();
addTimerUpdate();
if (myDeferredFocusRequest != null) {
final Runnable request = myDeferredFocusRequest;
myDeferredFocusRequest = null;
request.run();
}
}
@Override
public void removeNotify() {
super.removeNotify();
setFocused(false);
removeTimerUpdate();
if (ScreenUtil.isStandardAddRemoveNotify(this) && myGlassPane != null) {
myGlassPane.removeMouseMotionPreprocessor(myTabActionsAutoHideListener);
myGlassPane = null;
}
}
@Override
public void processMouseEvent(MouseEvent e) {
super.processMouseEvent(e);
}
private void addTimerUpdate() {
if (!myListenerAdded) {
myActionManager.addTimerListener(500, this);
myListenerAdded = true;
}
}
private void removeTimerUpdate() {
if (myListenerAdded) {
myActionManager.removeTimerListener(this);
myListenerAdded = false;
}
}
void setTestMode(final boolean testMode) {
myTestMode = testMode;
}
public void layoutComp(SingleRowPassInfo data, int deltaX, int deltaY, int deltaWidth, int deltaHeight) {
if (data.hToolbar != null) {
final int toolbarHeight = data.hToolbar.getPreferredSize().height;
final Rectangle compRect = layoutComp(deltaX, toolbarHeight + deltaY, data.comp, deltaWidth, deltaHeight);
layout(data.hToolbar, compRect.x, compRect.y - toolbarHeight, compRect.width, toolbarHeight);
}
else if (data.vToolbar != null) {
final int toolbarWidth = data.vToolbar.getPreferredSize().width;
final Rectangle compRect = layoutComp(toolbarWidth + deltaX, deltaY, data.comp, deltaWidth, deltaHeight);
layout(data.vToolbar, compRect.x - toolbarWidth, compRect.y, toolbarWidth, compRect.height);
}
else {
layoutComp(deltaX, deltaY, data.comp, deltaWidth, deltaHeight);
}
}
public boolean isDropTarget(TabInfo info) {
return myDropInfo != null && myDropInfo == info;
}
protected void setDropInfoIndex(int dropInfoIndex) {
myDropInfoIndex = dropInfoIndex;
}
public int getFirstTabOffset() {
return myFirstTabOffset;
}
public void setFirstTabOffset(int firstTabOffset) {
myFirstTabOffset = firstTabOffset;
}
class TabActionsAutoHideListener extends MouseMotionAdapter implements Weighted {
private TabLabel myCurrentOverLabel;
private Point myLastOverPoint;
@Override
public double getWeight() {
return 1;
}
@Override
public void mouseMoved(final MouseEvent e) {
if (!myTabLabelActionsAutoHide) return;
myLastOverPoint = SwingUtilities.convertPoint(e.getComponent(), e.getX(), e.getY(), JBTabsImpl.this);
processMouseOver();
}
void processMouseOver() {
if (!myTabLabelActionsAutoHide) return;
if (myLastOverPoint == null) return;
if (myLastOverPoint.x >= 0 && myLastOverPoint.x < getWidth() && myLastOverPoint.y > 0 && myLastOverPoint.y < getHeight()) {
final TabLabel label = myInfo2Label.get(_findInfo(myLastOverPoint, true));
if (label != null) {
if (myCurrentOverLabel != null) {
myCurrentOverLabel.toggleShowActions(false);
}
label.toggleShowActions(true);
myCurrentOverLabel = label;
return;
}
}
if (myCurrentOverLabel != null) {
myCurrentOverLabel.toggleShowActions(false);
myCurrentOverLabel = null;
}
}
}
@Override
public ModalityState getModalityState() {
return ModalityState.stateForComponent(this);
}
@Override
public void run() {
updateTabActions(false);
}
public void updateTabActions(final boolean validateNow) {
final Ref<Boolean> changed = new Ref<Boolean>(Boolean.FALSE);
for (final TabInfo eachInfo : myInfo2Label.keySet()) {
updateTab(new Computable<Boolean>() {
@Override
public Boolean compute() {
final boolean changes = myInfo2Label.get(eachInfo).updateTabActions();
changed.set(changed.get().booleanValue() || changes);
return changes;
}
}, eachInfo);
}
if (changed.get().booleanValue()) {
if (validateNow) {
validate();
paintImmediately(0, 0, getWidth(), getHeight());
}
}
}
public boolean canShowMorePopup() {
final SingleRowPassInfo lastLayout = mySingleRowLayout.myLastSingRowLayout;
return lastLayout != null && lastLayout.moreRect != null;
}
public void showMorePopup(@Nullable final MouseEvent e) {
final SingleRowPassInfo lastLayout = mySingleRowLayout.myLastSingRowLayout;
if (lastLayout == null) {
return;
}
mySingleRowLayout.myMorePopup = new JBPopupMenu();
for (final TabInfo each : myVisibleInfos) {
if (!mySingleRowLayout.isTabHidden(each)) continue;
final JBMenuItem item = new JBMenuItem(each.getText(), each.getIcon());
item.setForeground(each.getDefaultForeground());
item.setBackground(each.getTabColor());
mySingleRowLayout.myMorePopup.add(item);
item.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
select(each, true);
}
});
}
mySingleRowLayout.myMorePopup.addPopupMenuListener(new PopupMenuListener() {
@Override
public void popupMenuWillBecomeVisible(final PopupMenuEvent e) {
}
@Override
public void popupMenuWillBecomeInvisible(final PopupMenuEvent e) {
mySingleRowLayout.myMorePopup = null;
}
@Override
public void popupMenuCanceled(final PopupMenuEvent e) {
mySingleRowLayout.myMorePopup = null;
}
});
if (e != null) {
mySingleRowLayout.myMorePopup.show(this, e.getX(), e.getY());
}
else {
final Rectangle rect = lastLayout.moreRect;
if (rect != null) {
mySingleRowLayout.myMorePopup.show(this, rect.x, rect.y + rect.height);
}
}
}
@Nullable
private JComponent getToFocus() {
final TabInfo info = getSelectedInfo();
if (info == null) return null;
JComponent toFocus = null;
if (isRequestFocusOnLastFocusedComponent() && info.getLastFocusOwner() != null && !isMyChildIsFocusedNow()) {
toFocus = info.getLastFocusOwner();
}
if (toFocus == null && info.getPreferredFocusableComponent() == null) {
return null;
}
if (toFocus == null) {
toFocus = info.getPreferredFocusableComponent();
final JComponent policyToFocus = myFocusManager.getFocusTargetFor(toFocus);
if (policyToFocus != null) {
toFocus = policyToFocus;
}
}
return toFocus;
}
@Override
public void requestFocus() {
final JComponent toFocus = getToFocus();
if (toFocus != null) {
toFocus.requestFocus();
}
else {
super.requestFocus();
}
}
@Override
public boolean requestFocusInWindow() {
final JComponent toFocus = getToFocus();
if (toFocus != null) {
return toFocus.requestFocusInWindow();
}
else {
return super.requestFocusInWindow();
}
}
@Override
@NotNull
public TabInfo addTab(TabInfo info, int index) {
return addTab(info, index, false, true);
}
public TabInfo addTabSilently(TabInfo info, int index) {
return addTab(info, index, false, false);
}
private TabInfo addTab(TabInfo info, int index, boolean isDropTarget, boolean fireEvents) {
if (!isDropTarget && getTabs().contains(info)) {
return getTabs().get(getTabs().indexOf(info));
}
info.getChangeSupport().addPropertyChangeListener(this);
final TabLabel label = createTabLabel(info);
myInfo2Label.put(info, label);
if (!isDropTarget) {
if (index < 0 || index > myVisibleInfos.size() - 1) {
myVisibleInfos.add(info);
}
else {
myVisibleInfos.add(index, info);
}
}
resetTabsCache();
updateText(info);
updateIcon(info);
updateSideComponent(info);
updateTabActions(info);
add(label);
adjust(info);
updateAll(false, false);
if (info.isHidden()) {
updateHiding();
}
if (!isDropTarget && fireEvents) {
if (getTabCount() == 1) {
fireBeforeSelectionChanged(null, info);
fireSelectionChanged(null, info);
}
}
revalidateAndRepaint(false);
return info;
}
protected TabLabel createTabLabel(TabInfo info) {
return new TabLabel(this, info);
}
@Override
@NotNull
public TabInfo addTab(TabInfo info) {
return addTab(info, -1);
}
@Nullable
public ActionGroup getPopupGroup() {
return myPopupGroup != null ? myPopupGroup.get() : null;
}
public String getPopupPlace() {
return myPopupPlace;
}
@Override
@NotNull
public JBTabs setPopupGroup(@NotNull final ActionGroup popupGroup, @NotNull String place, final boolean addNavigationGroup) {
return setPopupGroup(new Getter<ActionGroup>() {
@Override
public ActionGroup get() {
return popupGroup;
}
}, place, addNavigationGroup);
}
@NotNull
public JBTabs setPopupGroup(@NotNull final Getter<ActionGroup> popupGroup,
@NotNull final String place,
final boolean addNavigationGroup) {
myPopupGroup = popupGroup;
myPopupPlace = place;
myAddNavigationGroup = addNavigationGroup;
return this;
}
private void updateAll(final boolean forcedRelayout, final boolean now) {
mySelectedInfo = getSelectedInfo();
updateContainer(forcedRelayout, now);
removeDeferred();
updateListeners();
updateTabActions(false);
updateEnabling();
}
private boolean isMyChildIsFocusedNow() {
final Component owner = getFocusOwner();
if (owner == null) return false;
if (mySelectedInfo != null) {
if (!SwingUtilities.isDescendingFrom(owner, mySelectedInfo.getComponent())) return false;
}
return SwingUtilities.isDescendingFrom(owner, this);
}
@Nullable
private static JComponent getFocusOwner() {
final Component owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
return (JComponent)(owner instanceof JComponent ? owner : null);
}
@Override
@NotNull
public ActionCallback select(@NotNull TabInfo info, boolean requestFocus) {
return _setSelected(info, requestFocus);
}
@NotNull
private ActionCallback _setSelected(final TabInfo info, final boolean requestFocus) {
if (mySelectionChangeHandler != null) {
return mySelectionChangeHandler.execute(info, requestFocus, new ActiveRunnable() {
@NotNull
@Override
public ActionCallback run() {
return executeSelectionChange(info, requestFocus);
}
});
}
else {
return executeSelectionChange(info, requestFocus);
}
}
@NotNull
private ActionCallback executeSelectionChange(TabInfo info, boolean requestFocus) {
if (mySelectedInfo != null && mySelectedInfo.equals(info)) {
if (!requestFocus) {
return new ActionCallback.Done();
}
else {
Component owner = myFocusManager.getFocusOwner();
JComponent c = info.getComponent();
if (c != null && owner != null) {
if (c == owner || SwingUtilities.isDescendingFrom(owner, c)) {
return new ActionCallback.Done();
}
}
return requestFocus(getToFocus());
}
}
if (myRequestFocusOnLastFocusedComponent && mySelectedInfo != null) {
if (isMyChildIsFocusedNow()) {
mySelectedInfo.setLastFocusOwner(getFocusOwner());
}
}
TabInfo oldInfo = mySelectedInfo;
mySelectedInfo = info;
final TabInfo newInfo = getSelectedInfo();
fireBeforeSelectionChanged(oldInfo, newInfo);
updateContainer(false, true);
fireSelectionChanged(oldInfo, newInfo);
if (requestFocus) {
final JComponent toFocus = getToFocus();
if (myProject != null && toFocus != null) {
final ActionCallback result = new ActionCallback();
requestFocus(toFocus).doWhenProcessed(new Runnable() {
@Override
public void run() {
if (myDisposed) {
result.setRejected();
}
else {
removeDeferred().notifyWhenDone(result);
}
}
});
return result;
}
else {
requestFocus();
return removeDeferred();
}
}
else {
return removeDeferred();
}
}
private void fireBeforeSelectionChanged(@Nullable TabInfo oldInfo, TabInfo newInfo) {
if (oldInfo != newInfo) {
myOldSelection = oldInfo;
try {
for (TabsListener eachListener : myTabListeners) {
eachListener.beforeSelectionChanged(oldInfo, newInfo);
}
}
finally {
myOldSelection = null;
}
}
}
private void fireSelectionChanged(@Nullable TabInfo oldInfo, TabInfo newInfo) {
if (oldInfo != newInfo) {
for (TabsListener eachListener : myTabListeners) {
if (eachListener != null) {
eachListener.selectionChanged(oldInfo, newInfo);
}
}
}
}
void fireTabsMoved() {
for (TabsListener eachListener : myTabListeners) {
if (eachListener != null) {
eachListener.tabsMoved();
}
}
}
void fireTabRemoved(TabInfo info) {
for (TabsListener eachListener : myTabListeners) {
if (eachListener != null) {
eachListener.tabRemoved(info);
}
}
}
@NotNull
private ActionCallback requestFocus(final JComponent toFocus) {
if (toFocus == null) return new ActionCallback.Done();
if (myTestMode) {
toFocus.requestFocus();
return new ActionCallback.Done();
}
if (isShowing()) {
return myFocusManager.requestFocus(new FocusCommand.ByComponent(toFocus, new Exception()), true);
}
else {
final ActionCallback result = new ActionCallback();
final FocusRequestor requestor = myFocusManager.getFurtherRequestor();
final Ref<Boolean> queued = new Ref<Boolean>(false);
Disposer.register(requestor, new Disposable() {
@Override
public void dispose() {
if (!queued.get()) {
result.setRejected();
}
}
});
myDeferredFocusRequest = new Runnable() {
@Override
public void run() {
queued.set(true);
requestor.requestFocus(new FocusCommand.ByComponent(toFocus, new Exception()), true).notify(result);
}
};
return result;
}
}
@NotNull
private ActionCallback removeDeferred() {
if (myDeferredToRemove.isEmpty()) {
return ActionCallback.DONE;
}
final ActionCallback callback = new ActionCallback();
final long executionRequest = ++myRemoveDeferredRequest;
final Runnable onDone = new Runnable() {
@Override
public void run() {
if (myRemoveDeferredRequest == executionRequest) {
removeDeferredNow();
}
callback.setDone();
}
};
myFocusManager.doWhenFocusSettlesDown(onDone);
return callback;
}
private void queueForRemove(Component c) {
if (c instanceof JComponent) {
addToDeferredRemove(c);
}
else {
remove(c);
}
}
private void unqueueFromRemove(Component c) {
myDeferredToRemove.remove(c);
}
private void removeDeferredNow() {
for (Component each : myDeferredToRemove.keySet()) {
if (each != null && each.getParent() == this) {
remove(each);
}
}
myDeferredToRemove.clear();
}
@Override
public void propertyChange(final PropertyChangeEvent evt) {
final TabInfo tabInfo = (TabInfo)evt.getSource();
if (TabInfo.ACTION_GROUP.equals(evt.getPropertyName())) {
updateSideComponent(tabInfo);
relayout(false, false);
}
else if (TabInfo.COMPONENT.equals(evt.getPropertyName())) {
relayout(true, false);
}
else if (TabInfo.TEXT.equals(evt.getPropertyName())) {
updateText(tabInfo);
}
else if (TabInfo.ICON.equals(evt.getPropertyName())) {
updateIcon(tabInfo);
}
else if (TabInfo.TAB_COLOR.equals(evt.getPropertyName())) {
updateColor(tabInfo);
}
else if (TabInfo.ALERT_STATUS.equals(evt.getPropertyName())) {
boolean start = ((Boolean)evt.getNewValue()).booleanValue();
updateAttraction(tabInfo, start);
}
else if (TabInfo.TAB_ACTION_GROUP.equals(evt.getPropertyName())) {
updateTabActions(tabInfo);
relayout(false, false);
}
else if (TabInfo.HIDDEN.equals(evt.getPropertyName())) {
updateHiding();
relayout(false, false);
}
else if (TabInfo.ENABLED.equals(evt.getPropertyName())) {
updateEnabling();
}
}
private void updateEnabling() {
final List<TabInfo> all = getTabs();
for (TabInfo each : all) {
final TabLabel eachLabel = myInfo2Label.get(each);
eachLabel.setTabEnabled(each.isEnabled());
}
final TabInfo selected = getSelectedInfo();
if (selected != null && !selected.isEnabled()) {
final TabInfo toSelect = getToSelectOnRemoveOf(selected);
if (toSelect != null) {
select(toSelect, myFocusManager.getFocusedDescendantFor(this) != null);
}
}
}
private void updateHiding() {
boolean update = false;
Iterator<TabInfo> visible = myVisibleInfos.iterator();
while (visible.hasNext()) {
TabInfo each = visible.next();
if (each.isHidden() && !myHiddenInfos.containsKey(each)) {
myHiddenInfos.put(each, myVisibleInfos.indexOf(each));
visible.remove();
update = true;
}
}
Iterator<TabInfo> hidden = myHiddenInfos.keySet().iterator();
while (hidden.hasNext()) {
TabInfo each = hidden.next();
if (!each.isHidden() && myHiddenInfos.containsKey(each)) {
myVisibleInfos.add(getIndexInVisibleArray(each), each);
hidden.remove();
update = true;
}
}
if (update) {
resetTabsCache();
if (mySelectedInfo != null && myHiddenInfos.containsKey(mySelectedInfo)) {
mySelectedInfo = getToSelectOnRemoveOf(mySelectedInfo);
}
updateAll(true, false);
}
}
private int getIndexInVisibleArray(TabInfo each) {
Integer index = myHiddenInfos.get(each);
if (index == null) {
index = Integer.valueOf(myVisibleInfos.size());
}
if (index > myVisibleInfos.size()) {
index = myVisibleInfos.size();
}
if (index.intValue() < 0) {
index = 0;
}
return index.intValue();
}
private void updateIcon(final TabInfo tabInfo) {
updateTab(new Computable<Boolean>() {
@Override
public Boolean compute() {
myInfo2Label.get(tabInfo).setIcon(tabInfo.getIcon());
return true;
}
}, tabInfo);
}
private void updateColor(final TabInfo tabInfo) {
myInfo2Label.get(tabInfo).setInactiveStateImage(null);
updateTab(new Computable<Boolean>() {
@Override
public Boolean compute() {
repaint();
return true;
}
}, tabInfo);
}
private void updateTab(Computable<Boolean> update, TabInfo info) {
final TabLabel label = myInfo2Label.get(info);
Boolean changes = update.compute();
if (label.getRootPane() != null) {
if (label.isValid()) {
if (changes) {
label.repaint();
}
}
else {
revalidateAndRepaint(false);
}
}
}
void revalidateAndRepaint(final boolean layoutNow) {
if (myVisibleInfos.isEmpty()) {
setOpaque(false);
final Component nonOpaque = UIUtil.findUltimateParent(this);
if (nonOpaque != null && getParent() != null) {
final Rectangle toRepaint = SwingUtilities.convertRectangle(getParent(), getBounds(), nonOpaque);
nonOpaque.repaint(toRepaint.x, toRepaint.y, toRepaint.width, toRepaint.height);
}
}
else {
setOpaque(true);
}
if (layoutNow) {
validate();
}
else {
revalidate();
}
repaint();
}
private void updateAttraction(final TabInfo tabInfo, boolean start) {
if (start) {
myAttractions.add(tabInfo);
}
else {
myAttractions.remove(tabInfo);
tabInfo.setBlinkCount(0);
}
if (start && !myAnimator.isRunning()) {
myAnimator.resume();
}
else if (!start && myAttractions.isEmpty()) {
myAnimator.suspend();
repaintAttractions();
}
}
private void updateText(final TabInfo tabInfo) {
updateTab(new Computable<Boolean>() {
@Override
public Boolean compute() {
final TabLabel label = myInfo2Label.get(tabInfo);
label.setText(tabInfo.getColoredText());
label.setToolTipText(tabInfo.getTooltipText());
return true;
}
}, tabInfo);
}
private void updateSideComponent(final TabInfo tabInfo) {
final Toolbar old = myInfo2Toolbar.get(tabInfo);
if (old != null) {
remove(old);
}
final Toolbar toolbar = createToolbarComponent(tabInfo);
myInfo2Toolbar.put(tabInfo, toolbar);
add(toolbar);
}
private void updateTabActions(final TabInfo info) {
myInfo2Label.get(info).setTabActions(info.getTabLabelActions());
}
@Override
@Nullable
public TabInfo getSelectedInfo() {
if (myOldSelection != null) return myOldSelection;
if (!myVisibleInfos.contains(mySelectedInfo)) {
mySelectedInfo = null;
}
return mySelectedInfo != null ? mySelectedInfo : !myVisibleInfos.isEmpty() ? myVisibleInfos.get(0) : null;
}
@Nullable
private TabInfo getToSelectOnRemoveOf(TabInfo info) {
if (!myVisibleInfos.contains(info)) return null;
if (mySelectedInfo != info) return null;
if (myVisibleInfos.size() == 1) return null;
int index = myVisibleInfos.indexOf(info);
TabInfo result = null;
if (index > 0) {
result = findEnabledBackward(index, false);
}
if (result == null) {
result = findEnabledForward(index, false);
}
return result;
}
@Nullable
private TabInfo findEnabledForward(int from, boolean cycle) {
if (from < 0) return null;
int index = from;
while (true) {
index++;
if (index == myVisibleInfos.size()) {
if (!cycle) break;
index = 0;
}
if (index == from) break;
final TabInfo each = myVisibleInfos.get(index);
if (each.isEnabled()) return each;
}
return null;
}
public boolean isAlphabeticalMode() {
return false;
}
@Nullable
private TabInfo findEnabledBackward(int from, boolean cycle) {
if (from < 0) return null;
int index = from;
while (true) {
index--;
if (index == -1) {
if (!cycle) break;
index = myVisibleInfos.size() - 1;
}
if (index == from) break;
final TabInfo each = myVisibleInfos.get(index);
if (each.isEnabled()) return each;
}
return null;
}
protected Toolbar createToolbarComponent(final TabInfo tabInfo) {
return new Toolbar(this, tabInfo);
}
@Override
@NotNull
public TabInfo getTabAt(final int tabIndex) {
return getTabs().get(tabIndex);
}
@NotNull
public List<TabInfo> getTabs() {
if (myAllTabs != null) return myAllTabs;
ArrayList<TabInfo> result = new ArrayList<TabInfo>();
result.addAll(myVisibleInfos);
for (TabInfo each : myHiddenInfos.keySet()) {
result.add(getIndexInVisibleArray(each), each);
}
myAllTabs = result;
return result;
}
@Override
public TabInfo getTargetInfo() {
return myPopupInfo != null ? myPopupInfo : getSelectedInfo();
}
@Override
public void popupMenuWillBecomeVisible(final PopupMenuEvent e) {
}
@Override
public void popupMenuWillBecomeInvisible(final PopupMenuEvent e) {
resetPopup();
}
@Override
public void popupMenuCanceled(final PopupMenuEvent e) {
resetPopup();
}
private void resetPopup() {
//todo [kirillk] dirty hack, should rely on ActionManager to understand that menu item was either chosen on or cancelled
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
myPopupInfo = null;
}
});
}
@Override
public void setPaintBlocked(boolean blocked, final boolean takeSnapshot) {
if (blocked && !myPaintBlocked) {
if (takeSnapshot) {
if (getWidth() > 0 && getHeight() > 0) {
myImage = UIUtil.createImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
final Graphics2D g = myImage.createGraphics();
super.paint(g);
g.dispose();
}
}
}
myPaintBlocked = blocked;
if (!myPaintBlocked) {
if (myImage != null) {
myImage.flush();
}
myImage = null;
repaint();
}
}
private void addToDeferredRemove(final Component c) {
if (!myDeferredToRemove.containsKey(c)) {
myDeferredToRemove.put(c, c);
}
}
private boolean isToDrawBorderIfTabsHidden() {
return myToDrawBorderIfTabsHidden;
}
@Override
@NotNull
public JBTabsPresentation setToDrawBorderIfTabsHidden(final boolean toDrawBorderIfTabsHidden) {
myToDrawBorderIfTabsHidden = toDrawBorderIfTabsHidden;
return this;
}
@Override
@NotNull
public JBTabs getJBTabs() {
return this;
}
public static class Toolbar extends JPanel {
private final JBTabsImpl myTabs;
public Toolbar(JBTabsImpl tabs, TabInfo info) {
myTabs = tabs;
setLayout(new BorderLayout());
final ActionGroup group = info.getGroup();
final JComponent side = info.getSideComponent();
if (group != null) {
final String place = info.getPlace();
ActionToolbar toolbar =
myTabs.myActionManager.createActionToolbar(place != null ? place : ActionPlaces.UNKNOWN, group, myTabs.myHorizontalSide);
toolbar.setTargetComponent(info.getActionsContextComponent());
final JComponent actionToolbar = toolbar.getComponent();
add(actionToolbar, BorderLayout.CENTER);
}
if (side != null) {
if (group != null) {
add(side, BorderLayout.EAST);
}
else {
add(side, BorderLayout.CENTER);
}
}
}
public boolean isEmpty() {
return getComponentCount() == 0;
}
}
@Override
public void doLayout() {
try {
myHeaderFitSize = computeHeaderFitSize();
final Collection<TabLabel> labels = myInfo2Label.values();
for (TabLabel each : labels) {
each.setTabActionsAutoHide(myTabLabelActionsAutoHide);
}
List<TabInfo> visible = new ArrayList<TabInfo>();
visible.addAll(myVisibleInfos);
if (myDropInfo != null && !visible.contains(myDropInfo) && myShowDropLocation) {
if (getDropInfoIndex() >= 0 && getDropInfoIndex() < visible.size()) {
visible.add(getDropInfoIndex(), myDropInfo);
}
else {
visible.add(myDropInfo);
}
}
if (isSingleRow()) {
myLastLayoutPass = mySingleRowLayout.layoutSingleRow(visible);
mySingleRowLayout.scroll(0);
myLastLayoutPass = mySingleRowLayout.layoutSingleRow(visible);
myTableLayout.myLastTableLayout = null;
}
else {
myLastLayoutPass = myTableLayout.layoutTable(visible);
mySingleRowLayout.myLastSingRowLayout = null;
}
if (isStealthModeEffective() && !isHideTabs()) {
final TabLabel label = getSelectedLabel();
final Rectangle bounds = label.getBounds();
final Insets insets = getLayoutInsets();
layout(label, insets.left, bounds.y, getWidth() - insets.right - insets.left, bounds.height);
}
moveDraggedTabLabel();
myTabActionsAutoHideListener.processMouseOver();
}
finally {
myForcedRelayout = false;
}
applyResetComponents();
}
void moveDraggedTabLabel() {
if (myDragHelper != null && myDragHelper.myDragRec != null) {
final TabLabel selectedLabel = myInfo2Label.get(getSelectedInfo());
if (selectedLabel != null) {
final Rectangle bounds = selectedLabel.getBounds();
if (isHorizontalTabs()) {
selectedLabel.setBounds(myDragHelper.myDragRec.x, bounds.y, bounds.width, bounds.height);
}
else {
selectedLabel.setBounds(bounds.x, myDragHelper.myDragRec.y, bounds.width, bounds.height);
}
}
}
}
private Dimension computeHeaderFitSize() {
final Max max = computeMaxSize();
if (myPosition == JBTabsPosition.top || myPosition == JBTabsPosition.bottom) {
return new Dimension(getSize().width, myHorizontalSide ? Math.max(max.myLabel.height, max.myToolbar.height) : max.myLabel.height);
}
else {
return new Dimension(max.myLabel.width + (myHorizontalSide ? 0 : max.myToolbar.width), getSize().height);
}
}
public Rectangle layoutComp(int componentX, int componentY, final JComponent comp, int deltaWidth, int deltaHeight) {
final Insets insets = getLayoutInsets();
final Insets border = isHideTabs() ? new Insets(0, 0, 0, 0) : myBorder.getEffectiveBorder();
final boolean noTabsVisible = isStealthModeEffective() || isHideTabs();
if (noTabsVisible) {
border.top = getBorder(-1);
border.bottom = getBorder(-1);
border.left = getBorder(-1);
border.right = getBorder(-1);
}
final Insets inner = getInnerInsets();
border.top += inner.top;
border.bottom += inner.bottom;
border.left += inner.left;
border.right += inner.right;
int x = insets.left + componentX + border.left;
int y = insets.top + componentY + border.top;
int width = getWidth() - insets.left - insets.right - componentX - border.left - border.right;
int height = getHeight() - insets.top - insets.bottom - componentY - border.top - border.bottom;
if (!noTabsVisible) {
width += deltaWidth;
height += deltaHeight;
}
return layout(comp, x, y, width, height);
}
@Override
public JBTabsPresentation setInnerInsets(final Insets innerInsets) {
myInnerInsets = innerInsets;
return this;
}
private Insets getInnerInsets() {
return myInnerInsets;
}
public Insets getLayoutInsets() {
Insets insets = getInsets();
if (insets == null) {
insets = new Insets(0, 0, 0, 0);
}
return insets;
}
public int getToolbarInset() {
return getArcSize() + 1;
}
public void resetLayout(boolean resetLabels) {
if (resetLabels) {
mySingleRowLayout.myLeftGhost.reset();
mySingleRowLayout.myRightGhost.reset();
}
for (TabInfo each : myVisibleInfos) {
reset(each, resetLabels);
}
if (myDropInfo != null) {
reset(myDropInfo, resetLabels);
}
for (TabInfo each : myHiddenInfos.keySet()) {
reset(each, resetLabels);
}
for (Component eachDeferred : myDeferredToRemove.keySet()) {
resetLayout((JComponent)eachDeferred);
}
}
private void reset(final TabInfo each, final boolean resetLabels) {
final JComponent c = each.getComponent();
if (c != null) {
resetLayout(c);
}
resetLayout(myInfo2Toolbar.get(each));
if (resetLabels) {
resetLayout(myInfo2Label.get(each));
}
}
private static int getArcSize() {
return 4;
}
private static int getEdgeArcSize() {
return 3;
}
public int getGhostTabLength() {
return 15;
}
protected JBTabsPosition getPosition() {
return myPosition;
}
protected void doPaintBackground(Graphics2D g2d, Rectangle clip) {
g2d.setColor(getBackground());
g2d.fill(clip);
}
@Override
protected void paintComponent(final Graphics g) {
super.paintComponent(g);
if (myVisibleInfos.isEmpty()) return;
Graphics2D g2d = (Graphics2D)g;
final GraphicsConfig config = new GraphicsConfig(g2d);
config.setAntialiasing(true);
final Rectangle clip = g2d.getClipBounds();
doPaintBackground(g2d, clip);
final TabInfo selected = getSelectedInfo();
if (selected != null) {
Rectangle compBounds = selected.getComponent().getBounds();
if (compBounds.contains(clip) && !compBounds.intersects(clip)) return;
}
boolean leftGhostExists = isSingleRow();
boolean rightGhostExists = isSingleRow();
if (!isStealthModeEffective() && !isHideTabs()) {
if (isSingleRow() && mySingleRowLayout.myLastSingRowLayout.lastGhostVisible) {
paintLastGhost(g2d);
}
paintNonSelectedTabs(g2d, leftGhostExists, rightGhostExists);
if (isSingleRow() && mySingleRowLayout.myLastSingRowLayout.firstGhostVisible) {
paintFirstGhost(g2d);
}
}
config.setAntialiasing(false);
Toolbar toolbarComp = myInfo2Toolbar.get(mySelectedInfo);
if (toolbarComp != null && !toolbarComp.isEmpty()) {
Rectangle toolBounds = toolbarComp.getBounds();
g2d.setColor(CaptionPanel.CNT_ACTIVE_BORDER_COLOR);
if (isSideComponentVertical()) {
g2d.drawLine((int)toolBounds.getMaxX(), toolBounds.y, (int)toolBounds.getMaxX(), (int)toolBounds.getMaxY() - 1);
} else if (!isSideComponentOnTabs()) {
g2d.drawLine(toolBounds.x, (int)toolBounds.getMaxY(), (int)toolBounds.getMaxX() - 1, (int)toolBounds.getMaxY());
}
}
config.restore();
}
@Nullable
protected Color getActiveTabColor(@Nullable final Color c) {
final TabInfo info = getSelectedInfo();
if (info == null) {
return c;
}
final Color tabColor = info.getTabColor();
return tabColor == null ? c : tabColor;
}
protected void paintSelectionAndBorder(Graphics2D g2d) {
if (mySelectedInfo == null) return;
final ShapeInfo shapeInfo = computeSelectedLabelShape();
if (!isHideTabs()) {
g2d.setColor(getBackground());
g2d.fill(shapeInfo.fillPath.getShape());
}
final int alpha;
int paintTopY = shapeInfo.labelTopY;
int paintBottomY = shapeInfo.labelBottomY;
final boolean paintFocused = myPaintFocus && (myFocused || myActivePopup != null || myAlwaysPaintSelectedTab);
Color bgPreFill = null;
if (paintFocused) {
final Color bgColor = getActiveTabColor(getActiveTabFillIn());
if (bgColor == null) {
shapeInfo.from = getFocusedTopFillColor();
shapeInfo.to = getFocusedBottomFillColor();
}
else {
bgPreFill = bgColor;
alpha = 255;
paintBottomY = shapeInfo.labelTopY + shapeInfo.labelPath.deltaY(getArcSize() - 2);
shapeInfo.from = ColorUtil.toAlpha(UIUtil.getFocusedFillColor(), alpha);
shapeInfo.to = ColorUtil.toAlpha(getActiveTabFillIn(), alpha);
}
}
else {
final Color bgColor = getActiveTabColor(getActiveTabFillIn());
if (isPaintFocus()) {
if (bgColor == null) {
alpha = 150;
shapeInfo.from = ColorUtil.toAlpha(UIUtil.getPanelBackground().brighter(), alpha);
shapeInfo.to = ColorUtil.toAlpha(UIUtil.getPanelBackground(), alpha);
}
else {
alpha = 255;
shapeInfo.from = ColorUtil.toAlpha(bgColor, alpha);
shapeInfo.to = ColorUtil.toAlpha(bgColor, alpha);
}
}
else {
alpha = 255;
final Color tabColor = getActiveTabColor(null);
final Color defaultBg = UIUtil.isUnderDarcula() ? UIUtil.getControlColor() : Color.white;
shapeInfo.from = tabColor == null ? defaultBg : tabColor;
shapeInfo.to = tabColor == null ? defaultBg : tabColor;
}
}
if (!isHideTabs()) {
if (bgPreFill != null) {
g2d.setColor(bgPreFill);
g2d.fill(shapeInfo.fillPath.getShape());
}
final Line2D.Float gradientLine =
shapeInfo.fillPath.transformLine(shapeInfo.fillPath.getX(), paintTopY, shapeInfo.fillPath.getX(), paintBottomY);
g2d.setPaint(UIUtil.getGradientPaint((float)gradientLine.getX1(), (float)gradientLine.getY1(),
shapeInfo.fillPath.transformY1(shapeInfo.from, shapeInfo.to), (float)gradientLine.getX2(),
(float)gradientLine.getY2(), shapeInfo.fillPath.transformY1(shapeInfo.to, shapeInfo.from)));
g2d.fill(shapeInfo.fillPath.getShape());
}
final Color tabColor = getActiveTabColor(null);
Color borderColor = tabColor == null ? UIUtil.getBoundsColor(paintFocused) : tabColor.darker();
g2d.setColor(borderColor);
if (!isHideTabs()) {
g2d.draw(shapeInfo.path.getShape());
}
paintBorder(g2d, shapeInfo, borderColor);
}
protected Color getFocusedTopFillColor() {
return UIUtil.getFocusedFillColor();
}
protected Color getFocusedBottomFillColor() {
return UIUtil.getFocusedFillColor();
}
protected ShapeInfo computeSelectedLabelShape() {
final ShapeInfo shape = new ShapeInfo();
shape.path = getEffectiveLayout().createShapeTransform(getSize());
shape.insets = shape.path.transformInsets(getLayoutInsets());
shape.labelPath = shape.path.createTransform(getSelectedLabel().getBounds());
shape.labelBottomY = shape.labelPath.getMaxY() + shape.labelPath.deltaY(1);
shape.labelTopY = shape.labelPath.getY();
shape.labelLeftX = shape.labelPath.getX();
shape.labelRightX = shape.labelPath.getX() + shape.labelPath.deltaX(shape.labelPath.getWidth());
Insets border = myBorder.getEffectiveBorder();
TabInfo selected = getSelectedInfo();
boolean first = myLastLayoutPass.getPreviousFor(selected) == null;
boolean last = myLastLayoutPass.getNextFor(selected) == null;
boolean leftEdge = !isSingleRow() && first && border.left == 0;
boolean rightEdge =
!isSingleRow() && last && Boolean.TRUE.equals(myInfo2Label.get(selected).getClientProperty(STRETCHED_BY_WIDTH)) && border.right == 0;
boolean isDraggedNow = selected != null && myDragHelper != null && selected.equals(myDragHelper.getDragSource());
if (leftEdge && !isDraggedNow) {
shape.path.moveTo(shape.insets.left, shape.labelTopY + shape.labelPath.deltaY(getEdgeArcSize()));
shape.path.quadTo(shape.labelLeftX, shape.labelTopY, shape.labelLeftX + shape.labelPath.deltaX(getEdgeArcSize()), shape.labelTopY);
shape.path.lineTo(shape.labelRightX - shape.labelPath.deltaX(getArcSize()), shape.labelTopY);
}
else {
shape.path.moveTo(shape.insets.left, shape.labelBottomY);
shape.path.lineTo(shape.labelLeftX, shape.labelBottomY);
shape.path.lineTo(shape.labelLeftX, shape.labelTopY + shape.labelPath.deltaY(getArcSize()));
shape.path.quadTo(shape.labelLeftX, shape.labelTopY, shape.labelLeftX + shape.labelPath.deltaX(getArcSize()), shape.labelTopY);
}
int lastX = shape.path.getWidth() - shape.path.deltaX(shape.insets.right + 1);
if (isStealthModeEffective()) {
shape.path.lineTo(lastX - shape.path.deltaX(getArcSize()), shape.labelTopY);
shape.path.quadTo(lastX, shape.labelTopY, lastX, shape.labelTopY + shape.path.deltaY(getArcSize()));
shape.path.lineTo(lastX, shape.labelBottomY);
}
else {
if (rightEdge) {
shape.path.lineTo(shape.labelRightX + 1 - shape.path.deltaX(getArcSize()), shape.labelTopY);
shape.path.quadTo(shape.labelRightX + 1, shape.labelTopY, shape.labelRightX + 1, shape.labelTopY + shape.path.deltaY(getArcSize()));
}
else {
shape.path.lineTo(shape.labelRightX - shape.path.deltaX(getArcSize()), shape.labelTopY);
shape.path.quadTo(shape.labelRightX, shape.labelTopY, shape.labelRightX, shape.labelTopY + shape.path.deltaY(getArcSize()));
}
if (myLastLayoutPass.hasCurveSpaceFor(selected)) {
shape.path.lineTo(shape.labelRightX, shape.labelBottomY - shape.path.deltaY(getArcSize()));
shape.path.quadTo(shape.labelRightX, shape.labelBottomY, shape.labelRightX + shape.path.deltaX(getArcSize()), shape.labelBottomY);
}
else {
if (rightEdge) {
shape.path.lineTo(shape.labelRightX + 1, shape.labelBottomY);
}
else {
shape.path.lineTo(shape.labelRightX, shape.labelBottomY);
}
}
}
if (!rightEdge) {
shape.path.lineTo(lastX, shape.labelBottomY);
}
if (isStealthModeEffective()) {
shape.path.closePath();
}
shape.fillPath = shape.path.copy();
if (!isHideTabs()) {
shape.fillPath.lineTo(lastX, shape.labelBottomY + shape.fillPath.deltaY(1));
shape.fillPath.lineTo(shape.labelLeftX, shape.labelBottomY + shape.fillPath.deltaY(1));
shape.fillPath.closePath();
}
return shape;
}
protected TabLabel getSelectedLabel() {
return myInfo2Label.get(getSelectedInfo());
}
protected static class ShapeInfo {
public ShapeInfo() {}
public ShapeTransform path;
public ShapeTransform fillPath;
public ShapeTransform labelPath;
public int labelBottomY;
public int labelTopY;
public int labelLeftX;
public int labelRightX;
public Insets insets;
public Color from;
public Color to;
}
protected void paintFirstGhost(Graphics2D g2d) {
final ShapeTransform path = getEffectiveLayout().createShapeTransform(mySingleRowLayout.myLastSingRowLayout.firstGhost);
int topX = path.getX() + path.deltaX(getCurveArc());
int topY = path.getY() + path.deltaY(getSelectionTabVShift());
int bottomX = path.getMaxX() + path.deltaX(1);
int bottomY = path.getMaxY() + path.deltaY(1);
path.moveTo(topX, topY);
final boolean isLeftFromSelection = mySingleRowLayout.myLastSingRowLayout.toLayout.indexOf(getSelectedInfo()) == 0;
if (isLeftFromSelection) {
path.lineTo(bottomX, topY);
}
else {
path.lineTo(bottomX - getArcSize(), topY);
path.quadTo(bottomX, topY, bottomX, topY + path.deltaY(getArcSize()));
}
path.lineTo(bottomX, bottomY);
path.lineTo(topX, bottomY);
path.quadTo(topX - path.deltaX(getCurveArc() * 2 - 1), bottomY - path.deltaY(Math.abs(bottomY - topY) / 4), topX,
bottomY - path.deltaY(Math.abs(bottomY - topY) / 2));
path.quadTo(topX + path.deltaX(getCurveArc() - 1), topY + path.deltaY(Math.abs(bottomY - topY) / 4), topX, topY);
path.closePath();
g2d.setColor(getBackground());
g2d.fill(path.getShape());
g2d.setColor(getBoundsColor());
g2d.draw(path.getShape());
g2d.setColor(getTopBlockColor());
g2d.drawLine(topX + path.deltaX(1), topY + path.deltaY(1), bottomX - path.deltaX(getArcSize()), topY + path.deltaY(1));
g2d.setColor(getRightBlockColor());
g2d.drawLine(bottomX - path.deltaX(1), topY + path.deltaY(getArcSize()), bottomX - path.deltaX(1), bottomY - path.deltaY(1));
}
protected void paintLastGhost(Graphics2D g2d) {
final ShapeTransform path = getEffectiveLayout().createShapeTransform(mySingleRowLayout.myLastSingRowLayout.lastGhost);
int topX = path.getX() - path.deltaX(getArcSize());
int topY = path.getY() + path.deltaY(getSelectionTabVShift());
int bottomX = path.getMaxX() - path.deltaX(getCurveArc());
int bottomY = path.getMaxY() + path.deltaY(1);
path.moveTo(topX, topY);
path.lineTo(bottomX, topY);
path.quadTo(bottomX - getCurveArc(), topY + (bottomY - topY) / 4, bottomX, topY + (bottomY - topY) / 2);
path.quadTo(bottomX + getCurveArc(), bottomY - (bottomY - topY) / 4, bottomX, bottomY);
path.lineTo(topX, bottomY);
path.closePath();
g2d.setColor(getBackground());
g2d.fill(path.getShape());
g2d.setColor(getBoundsColor());
g2d.draw(path.getShape());
g2d.setColor(getTopBlockColor());
g2d.drawLine(topX, topY + path.deltaY(1), bottomX - path.deltaX(getCurveArc()), topY + path.deltaY(1));
}
private static int getCurveArc() {
return 2;
}
private static Color getBoundsColor() {
return new JBColor(Color.gray, Gray._0.withAlpha(80));
}
private static Color getRightBlockColor() {
return new JBColor(Color.lightGray, Gray._0.withAlpha(0));
}
private static Color getTopBlockColor() {
return new JBColor(Color.white, Gray._0.withAlpha(0));
}
private void paintNonSelectedTabs(final Graphics2D g2d, final boolean leftGhostExists, final boolean rightGhostExists) {
TabInfo selected = getSelectedInfo();
if (myLastPaintedSelection == null || !myLastPaintedSelection.equals(selected)) {
List<TabInfo> tabs = getTabs();
for (TabInfo each : tabs) {
myInfo2Label.get(each).setInactiveStateImage(null);
}
}
for (int eachRow = 0; eachRow < myLastLayoutPass.getRowCount(); eachRow++) {
for (int eachColumn = myLastLayoutPass.getColumnCount(eachRow) - 1; eachColumn >= 0; eachColumn--) {
final TabInfo each = myLastLayoutPass.getTabAt(eachRow, eachColumn);
if (getSelectedInfo() == each) {
continue;
}
paintNonSelected(g2d, each, leftGhostExists, rightGhostExists, eachRow, eachColumn);
}
}
myLastPaintedSelection = selected;
}
private void paintNonSelected(final Graphics2D g2d,
final TabInfo each,
final boolean leftGhostExists,
final boolean rightGhostExists,
int row, int column) {
if (myDropInfo == each) return;
final TabLabel label = myInfo2Label.get(each);
if (label.getBounds().width == 0) return;
int imageInsets = getArcSize() + 1;
Rectangle bounds = label.getBounds();
int x = bounds.x - imageInsets;
int y = bounds.y;
int width = bounds.width + imageInsets * 2 + 1;
int height = bounds.height + getArcSize() + 1;
if (isToBufferPainting()) {
BufferedImage img = label.getInactiveStateImage(bounds);
if (img == null) {
img = UIUtil.createImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D imgG2d = img.createGraphics();
imgG2d.addRenderingHints(g2d.getRenderingHints());
doPaintInactive(imgG2d, leftGhostExists, label, new Rectangle(imageInsets, 0, label.getWidth(), label.getHeight()),
rightGhostExists, row, column);
imgG2d.dispose();
}
g2d.drawImage(img, x, y, width, height, null);
label.setInactiveStateImage(img);
}
else {
doPaintInactive(g2d, leftGhostExists, label, label.getBounds(), rightGhostExists, row, column);
label.setInactiveStateImage(null);
}
}
private boolean isToBufferPainting() {
return Registry.is("ide.tabbedPane.bufferedPaint") && myUseBufferedPaint;
}
protected List<TabInfo> getVisibleInfos() {
return myVisibleInfos;
}
protected LayoutPassInfo getLastLayoutPass() {
return myLastLayoutPass;
}
@Override
public Color getBackground() {
return UIUtil.isUnderNimbusLookAndFeel() ? UIUtil.getPanelBackground() : super.getBackground();
}
protected void doPaintInactive(Graphics2D g2d,
boolean leftGhostExists,
TabLabel label,
Rectangle effectiveBounds,
boolean rightGhostExists, int row, int column) {
int tabIndex = myVisibleInfos.indexOf(label.getInfo());
final int arc = getArcSize();
Color topBlockColor = getTopBlockColor();
Color rightBlockColor = getRightBlockColor();
Color boundsColor = getBoundsColor();
Color backgroundColor = getBackground();
final Color tabColor = label.getInfo().getTabColor();
if (tabColor != null) {
backgroundColor = tabColor;
boundsColor = tabColor.darker();
topBlockColor = tabColor.brighter().brighter();
rightBlockColor = tabColor;
}
final TabInfo selected = getSelectedInfo();
final int selectionTabVShift = getSelectionTabVShift();
final TabInfo prev = myLastLayoutPass.getPreviousFor(myVisibleInfos.get(tabIndex));
final TabInfo next = myLastLayoutPass.getNextFor(myVisibleInfos.get(tabIndex));
boolean firstShowing = prev == null;
if (!firstShowing && !leftGhostExists) {
firstShowing = myInfo2Label.get(prev).getBounds().width == 0;
}
boolean lastShowing = next == null;
if (!lastShowing) {
lastShowing = myInfo2Label.get(next).getBounds().width == 0;
}
boolean leftFromSelection = selected != null && tabIndex == myVisibleInfos.indexOf(selected) - 1;
final ShapeTransform shape = getEffectiveLayout().createShapeTransform(effectiveBounds);
int leftX = firstShowing ? shape.getX() : shape.getX() - shape.deltaX(arc + 1);
int topY = shape.getY() + shape.deltaY(selectionTabVShift);
int rightX = !lastShowing && leftFromSelection ? shape.getMaxX() + shape.deltaX(arc + 1) : shape.getMaxX();
int bottomY = shape.getMaxY() + shape.deltaY(1);
Insets border = myBorder.getEffectiveBorder();
if (border.left > 0 || leftGhostExists || !firstShowing) {
shape.moveTo(leftX, bottomY);
shape.lineTo(leftX, topY + shape.deltaY(arc));
shape.quadTo(leftX, topY, leftX + shape.deltaX(arc), topY);
}
else {
if (firstShowing) {
shape.moveTo(leftX, topY + shape.deltaY(getEdgeArcSize()));
shape.quadTo(leftX, topY, leftX + shape.deltaX(getEdgeArcSize()), topY);
}
}
boolean rightEdge = false;
if (border.right > 0 || rightGhostExists || !lastShowing || !Boolean.TRUE.equals(label.getClientProperty(STRETCHED_BY_WIDTH))) {
shape.lineTo(rightX - shape.deltaX(arc), topY);
shape.quadTo(rightX, topY, rightX, topY + shape.deltaY(arc));
shape.lineTo(rightX, bottomY);
}
else {
if (lastShowing) {
shape.lineTo(rightX - shape.deltaX(arc), topY);
shape.quadTo(rightX + 1, topY, rightX + 1, topY + shape.deltaY(arc));
shape.lineTo(rightX + 1, bottomY);
rightEdge = true;
}
}
if (!isSingleRow()) {
final TablePassInfo info = myTableLayout.myLastTableLayout;
if (!info.isInSelectionRow(label.getInfo())) {
shape.lineTo(rightX, bottomY + shape.deltaY(getArcSize()));
shape.lineTo(leftX, bottomY + shape.deltaY(getArcSize()));
shape.lineTo(leftX, bottomY);
}
}
if (!rightEdge) {
shape.lineTo(leftX, bottomY);
}
g2d.setColor(backgroundColor);
g2d.fill(shape.getShape());
// TODO
final Line2D.Float gradientLine =
shape.transformLine(0, topY, 0, topY + shape.deltaY((int)(shape.getHeight() / 1.5)));
final Paint gp = UIUtil.isUnderDarcula()
? UIUtil.getGradientPaint(gradientLine.x1, gradientLine.y1,
shape.transformY1(backgroundColor, backgroundColor),
gradientLine.x2, gradientLine.y2,
shape.transformY1(backgroundColor, backgroundColor))
: UIUtil.getGradientPaint(gradientLine.x1, gradientLine.y1,
shape.transformY1(backgroundColor.brighter().brighter(), backgroundColor),
gradientLine.x2, gradientLine.y2,
shape.transformY1(backgroundColor, backgroundColor.brighter().brighter()));
final Paint old = g2d.getPaint();
g2d.setPaint(gp);
g2d.fill(shape.getShape());
g2d.setPaint(old);
g2d.setColor(topBlockColor);
g2d.draw(
shape.transformLine(leftX + shape.deltaX(arc + 1), topY + shape.deltaY(1), rightX - shape.deltaX(arc - 1), topY + shape.deltaY(1)));
if (!rightEdge) {
g2d.setColor(rightBlockColor);
g2d.draw(shape.transformLine(rightX - shape.deltaX(1), topY + shape.deltaY(arc - 1), rightX - shape.deltaX(1), bottomY));
}
g2d.setColor(boundsColor);
g2d.draw(shape.getShape());
}
public static int getSelectionTabVShift() {
return 2;
}
protected void paintBorder(Graphics2D g2d, ShapeInfo shape, final Color borderColor) {
final ShapeTransform shaper = shape.path.copy().reset();
final Insets paintBorder = shape.path.transformInsets(myBorder.getEffectiveBorder());
int topY = shape.labelPath.getMaxY() + shape.labelPath.deltaY(1);
int bottomY = topY + paintBorder.top - 2;
int middleY = topY + (bottomY - topY) / 2;
final int boundsX = shape.path.getX() + shape.path.deltaX(shape.insets.left);
final int boundsY =
isHideTabs() ? shape.path.getY() + shape.path.deltaY(shape.insets.top) : shape.labelPath.getMaxY() + shape.path.deltaY(1);
final int boundsHeight = Math.abs(shape.path.getMaxY() - boundsY) - shape.insets.bottom - paintBorder.bottom;
final int boundsWidth = Math.abs(shape.path.getMaxX() - (shape.insets.left + shape.insets.right));
if (paintBorder.top > 0) {
if (isHideTabs()) {
if (isToDrawBorderIfTabsHidden()) {
g2d.setColor(borderColor);
g2d.fill(shaper.reset().doRect(boundsX, boundsY, boundsWidth, 1).getShape());
}
}
else {
Color tabFillColor = getActiveTabColor(null);
if (tabFillColor == null) {
tabFillColor = shape.path.transformY1(shape.to, shape.from);
}
g2d.setColor(tabFillColor);
g2d.fill(shaper.reset().doRect(boundsX, topY + shape.path.deltaY(1), boundsWidth, paintBorder.top - 1).getShape());
g2d.setColor(borderColor);
if (paintBorder.top == 2) {
final Line2D.Float line = shape.path.transformLine(boundsX, topY, boundsX + shape.path.deltaX(boundsWidth - 1), topY);
g2d.drawLine((int)line.x1, (int)line.y1, (int)line.x2, (int)line.y2);
}
else if (paintBorder.top > 2) {
//todo kirillk
//start hack
int deltaY = 0;
if (myPosition == JBTabsPosition.bottom || myPosition == JBTabsPosition.right) {
deltaY = 1;
}
//end hack
final int topLine = topY + shape.path.deltaY(paintBorder.top - 1);
g2d.fill(shaper.reset().doRect(boundsX, topLine + deltaY, boundsWidth - 1, 1).getShape());
}
}
}
g2d.setColor(borderColor);
//bottom
g2d.fill(shaper.reset().doRect(boundsX, Math.abs(shape.path.getMaxY() - shape.insets.bottom - paintBorder.bottom), boundsWidth,
paintBorder.bottom).getShape());
//left
g2d.fill(shaper.reset().doRect(boundsX, boundsY, paintBorder.left, boundsHeight).getShape());
//right
g2d.fill(shaper.reset()
.doRect(shape.path.getMaxX() - shape.insets.right - paintBorder.right, boundsY, paintBorder.right, boundsHeight).getShape());
}
protected boolean isStealthModeEffective() {
return myStealthTabMode && getTabCount() == 1 &&
(isSideComponentVertical() || !isSideComponentOnTabs()) &&
getTabsPosition() == JBTabsPosition.top;
}
private boolean isNavigationVisible() {
if (myStealthTabMode && getTabCount() == 1) return false;
return !myVisibleInfos.isEmpty();
}
@Override
public void paint(final Graphics g) {
Rectangle clip = g.getClipBounds();
if (clip == null) {
return;
}
if (myPaintBlocked) {
if (myImage != null) {
g.drawImage(myImage, 0, 0, getWidth(), getHeight(), null);
}
return;
}
super.paint(g);
}
@Override
protected void paintChildren(final Graphics g) {
super.paintChildren(g);
final GraphicsConfig config = new GraphicsConfig(g);
config.setAntialiasing(true);
paintSelectionAndBorder((Graphics2D)g);
config.restore();
final TabLabel selected = getSelectedLabel();
if (selected != null) {
selected.paintImage(g);
}
mySingleRowLayout.myMoreIcon.paintIcon(this, g);
}
private Max computeMaxSize() {
Max max = new Max();
for (TabInfo eachInfo : myVisibleInfos) {
final TabLabel label = myInfo2Label.get(eachInfo);
max.myLabel.height = Math.max(max.myLabel.height, label.getPreferredSize().height);
max.myLabel.width = Math.max(max.myLabel.width, label.getPreferredSize().width);
final Toolbar toolbar = myInfo2Toolbar.get(eachInfo);
if (myLayout.isSideComponentOnTabs() && toolbar != null && !toolbar.isEmpty()) {
max.myToolbar.height = Math.max(max.myToolbar.height, toolbar.getPreferredSize().height);
max.myToolbar.width = Math.max(max.myToolbar.width, toolbar.getPreferredSize().width);
}
}
max.myToolbar.height++;
return max;
}
@Override
public Dimension getMinimumSize() {
return computeSize(new Function<JComponent, Dimension>() {
@Override
public Dimension fun(JComponent component) {
return component.getMinimumSize();
}
}, 1);
}
@Override
public Dimension getPreferredSize() {
return computeSize(new Function<JComponent, Dimension>() {
@Override
public Dimension fun(JComponent component) {
return component.getPreferredSize();
}
}, 3);
}
private Dimension computeSize(Function<JComponent, Dimension> transform, int tabCount) {
Dimension size = new Dimension();
for (TabInfo each : myVisibleInfos) {
final JComponent c = each.getComponent();
if (c != null) {
final Dimension eachSize = transform.fun(c);
size.width = Math.max(eachSize.width, size.width);
size.height = Math.max(eachSize.height, size.height);
}
}
addHeaderSize(size, tabCount);
return size;
}
private void addHeaderSize(Dimension size, final int tabsCount) {
Dimension header = computeHeaderPreferredSize(tabsCount);
final boolean horizontal = getTabsPosition() == JBTabsPosition.top || getTabsPosition() == JBTabsPosition.bottom;
if (horizontal) {
size.height += header.height;
size.width = Math.max(size.width, header.width);
}
else {
size.height += Math.max(size.height, header.height);
size.width += header.width;
}
final Insets insets = getLayoutInsets();
size.width += insets.left + insets.right + 1;
size.height += insets.top + insets.bottom + 1;
}
private Dimension computeHeaderPreferredSize(int tabsCount) {
final Iterator<TabInfo> infos = myInfo2Label.keySet().iterator();
Dimension size = new Dimension();
int currentTab = 0;
final boolean horizontal = getTabsPosition() == JBTabsPosition.top || getTabsPosition() == JBTabsPosition.bottom;
while (infos.hasNext()) {
final boolean canGrow = currentTab < tabsCount;
TabInfo eachInfo = infos.next();
final TabLabel eachLabel = myInfo2Label.get(eachInfo);
final Dimension eachPrefSize = eachLabel.getPreferredSize();
if (horizontal) {
if (canGrow) {
size.width += eachPrefSize.width;
}
size.height = Math.max(size.height, eachPrefSize.height);
}
else {
size.width = Math.max(size.width, eachPrefSize.width);
if (canGrow) {
size.height += eachPrefSize.height;
}
}
currentTab++;
}
if (isSingleRow() && isGhostsAlwaysVisible()) {
if (horizontal) {
size.width += getGhostTabLength() * 2;
}
else {
size.height += getGhostTabLength() * 2;
}
}
if (horizontal) {
size.height += myBorder.getTabBorderSize();
}
else {
size.width += myBorder.getTabBorderSize();
}
return size;
}
@Override
public int getTabCount() {
return getTabs().size();
}
@Override
@NotNull
public JBTabsPresentation getPresentation() {
return this;
}
@Override
@NotNull
public ActionCallback removeTab(final TabInfo info) {
return removeTab(info, null, true);
}
@NotNull
public ActionCallback removeTab(final TabInfo info, @Nullable TabInfo forcedSelectionTransfer, boolean transferFocus) {
return removeTab(info, forcedSelectionTransfer, transferFocus, false);
}
@NotNull
private ActionCallback removeTab(TabInfo info, @Nullable TabInfo forcedSelectionTransfer, boolean transferFocus, boolean isDropTarget) {
if (!isDropTarget) {
if (info == null || !getTabs().contains(info)) return new ActionCallback.Done();
}
if (isDropTarget && myLastLayoutPass != null) {
myLastLayoutPass.myVisibleInfos.remove(info);
}
final ActionCallback result = new ActionCallback();
TabInfo toSelect;
if (forcedSelectionTransfer == null) {
toSelect = getToSelectOnRemoveOf(info);
}
else {
assert myVisibleInfos.contains(forcedSelectionTransfer) : "Cannot find tab for selection transfer, tab=" + forcedSelectionTransfer;
toSelect = forcedSelectionTransfer;
}
if (toSelect != null) {
boolean clearSelection = info.equals(mySelectedInfo);
processRemove(info, false);
if (clearSelection) {
mySelectedInfo = info;
}
_setSelected(toSelect, transferFocus).doWhenProcessed(new Runnable() {
@Override
public void run() {
removeDeferred().notifyWhenDone(result);
}
});
}
else {
processRemove(info, true);
removeDeferred().notifyWhenDone(result);
}
if (myVisibleInfos.isEmpty()) {
removeDeferredNow();
}
revalidateAndRepaint(true);
fireTabRemoved(info);
return result;
}
private void processRemove(final TabInfo info, boolean forcedNow) {
remove(myInfo2Label.get(info));
remove(myInfo2Toolbar.get(info));
JComponent tabComponent = info.getComponent();
if (!isToDeferRemoveForLater(tabComponent) || forcedNow) {
remove(tabComponent);
}
else {
queueForRemove(tabComponent);
}
myVisibleInfos.remove(info);
myHiddenInfos.remove(info);
myInfo2Label.remove(info);
myInfo2Toolbar.remove(info);
resetTabsCache();
updateAll(false, false);
// avoid leaks
myLastPaintedSelection = null;
}
@Nullable
public TabInfo findInfo(Component component) {
for (TabInfo each : getTabs()) {
if (each.getComponent() == component) return each;
}
return null;
}
@Override
public TabInfo findInfo(MouseEvent event) {
return findInfo(event, false);
}
@Nullable
private TabInfo findInfo(final MouseEvent event, final boolean labelsOnly) {
final Point point = SwingUtilities.convertPoint(event.getComponent(), event.getPoint(), this);
return _findInfo(point, labelsOnly);
}
@Override
public TabInfo findInfo(final Object object) {
for (int i = 0; i < getTabCount(); i++) {
final TabInfo each = getTabAt(i);
final Object eachObject = each.getObject();
if (eachObject != null && eachObject.equals(object)) return each;
}
return null;
}
@Nullable
private TabInfo _findInfo(final Point point, boolean labelsOnly) {
Component component = findComponentAt(point);
if (component == null) return null;
while (component != this || component != null) {
if (component instanceof TabLabel) {
return ((TabLabel)component).getInfo();
}
if (!labelsOnly) {
final TabInfo info = findInfo(component);
if (info != null) return info;
}
if (component == null) break;
component = component.getParent();
}
return null;
}
@Override
public void removeAllTabs() {
for (TabInfo each : getTabs()) {
removeTab(each);
}
}
private static class Max {
final Dimension myLabel = new Dimension();
final Dimension myToolbar = new Dimension();
}
private void updateContainer(boolean forced, final boolean layoutNow) {
if (myProject != null && !myProject.isOpen()) return;
for (TabInfo each : new ArrayList<TabInfo>(myVisibleInfos)) {
final JComponent eachComponent = each.getComponent();
if (getSelectedInfo() == each && getSelectedInfo() != null) {
unqueueFromRemove(eachComponent);
final Container parent = eachComponent.getParent();
if (parent != null && parent != this) {
parent.remove(eachComponent);
}
if (eachComponent.getParent() == null) {
add(eachComponent);
}
}
else {
if (eachComponent.getParent() == null) continue;
if (isToDeferRemoveForLater(eachComponent)) {
queueForRemove(eachComponent);
}
else {
remove(eachComponent);
}
}
}
mySingleRowLayout.scrollSelectionInView();
relayout(forced, layoutNow);
}
@Override
protected void addImpl(final Component comp, final Object constraints, final int index) {
unqueueFromRemove(comp);
if (comp instanceof TabLabel) {
((TabLabel)comp).apply(myUiDecorator.getDecoration());
}
super.addImpl(comp, constraints, index);
}
private static boolean isToDeferRemoveForLater(JComponent c) {
return c.getRootPane() != null;
}
void relayout(boolean forced, final boolean layoutNow) {
if (!myForcedRelayout) {
myForcedRelayout = forced;
}
revalidateAndRepaint(layoutNow);
}
public TabsBorder getTabsBorder() {
return myBorder;
}
@Override
@NotNull
public JBTabs addTabMouseListener(@NotNull MouseListener listener) {
removeListeners();
myTabMouseListeners.add(listener);
addListeners();
return this;
}
@Override
@NotNull
public JComponent getComponent() {
return this;
}
@Override
public boolean isCycleRoot() {
return false;
}
private void addListeners() {
for (TabInfo eachInfo : myVisibleInfos) {
final TabLabel label = myInfo2Label.get(eachInfo);
for (EventListener eachListener : myTabMouseListeners) {
if (eachListener instanceof MouseListener) {
label.addMouseListener((MouseListener)eachListener);
}
else if (eachListener instanceof MouseMotionListener) {
label.addMouseMotionListener((MouseMotionListener)eachListener);
}
else {
assert false;
}
}
}
}
private void removeListeners() {
for (TabInfo eachInfo : myVisibleInfos) {
final TabLabel label = myInfo2Label.get(eachInfo);
for (EventListener eachListener : myTabMouseListeners) {
if (eachListener instanceof MouseListener) {
label.removeMouseListener((MouseListener)eachListener);
}
else if (eachListener instanceof MouseMotionListener) {
label.removeMouseMotionListener((MouseMotionListener)eachListener);
}
else {
assert false;
}
}
}
}
private void updateListeners() {
removeListeners();
addListeners();
}
@Override
public JBTabs addListener(@NotNull TabsListener listener) {
myTabListeners.add(listener);
return this;
}
@Override
public JBTabs setSelectionChangeHandler(SelectionChangeHandler handler) {
mySelectionChangeHandler = handler;
return this;
}
public void setFocused(final boolean focused) {
if (myFocused == focused) return;
myFocused = focused;
if (myPaintFocus) {
repaint();
}
}
@Override
public int getIndexOf(@Nullable final TabInfo tabInfo) {
return myVisibleInfos.indexOf(tabInfo);
}
@Override
public boolean isHideTabs() {
return myHideTabs;
}
@Override
public void setHideTabs(final boolean hideTabs) {
if (isHideTabs() == hideTabs) return;
myHideTabs = hideTabs;
relayout(true, false);
}
@Override
public JBTabsPresentation setPaintBorder(int top, int left, int right, int bottom) {
return myBorder.setPaintBorder(top, left, right, bottom);
}
@Override
public JBTabsPresentation setTabSidePaintBorder(int size) {
return myBorder.setTabSidePaintBorder(size);
}
static int getBorder(int size) {
return size == -1 ? 1 : size;
}
private boolean isPaintFocus() {
return myPaintFocus;
}
@Override
@NotNull
public JBTabsPresentation setActiveTabFillIn(@Nullable final Color color) {
if (!isChanged(myActiveTabFillIn, color)) return this;
myActiveTabFillIn = color;
revalidateAndRepaint(false);
return this;
}
private static boolean isChanged(Object oldObject, Object newObject) {
return !Comparing.equal(oldObject, newObject);
}
@Override
@NotNull
public JBTabsPresentation setTabLabelActionsAutoHide(final boolean autoHide) {
if (myTabLabelActionsAutoHide != autoHide) {
myTabLabelActionsAutoHide = autoHide;
revalidateAndRepaint(false);
}
return this;
}
@Nullable
protected Color getActiveTabFillIn() {
return myActiveTabFillIn;
}
@Override
public JBTabsPresentation setFocusCycle(final boolean root) {
setFocusCycleRoot(root);
return this;
}
@Override
public JBTabsPresentation setPaintFocus(final boolean paintFocus) {
myPaintFocus = paintFocus;
return this;
}
@Override
public JBTabsPresentation setAlwaysPaintSelectedTab(final boolean paintSelected) {
myAlwaysPaintSelectedTab = paintSelected;
return this;
}
private abstract static class BaseNavigationAction extends AnAction {
private final ShadowAction myShadow;
@NotNull private final ActionManager myActionManager;
private final JBTabsImpl myTabs;
protected BaseNavigationAction(@NotNull String copyFromID, @NotNull JBTabsImpl tabs, @NotNull ActionManager mgr) {
myActionManager = mgr;
myTabs = tabs;
myShadow = new ShadowAction(this, myActionManager.getAction(copyFromID), tabs);
Disposer.register(tabs, myShadow);
setEnabledInModalContext(true);
}
@Override
public final void update(final AnActionEvent e) {
JBTabsImpl tabs = e.getData(NAVIGATION_ACTIONS_KEY);
e.getPresentation().setVisible(tabs != null);
if (tabs == null) return;
tabs = findNavigatableTabs(tabs);
e.getPresentation().setEnabled(tabs != null);
if (tabs != null) {
_update(e, tabs, tabs.myVisibleInfos.indexOf(tabs.getSelectedInfo()));
}
}
@Nullable
protected JBTabsImpl findNavigatableTabs(JBTabsImpl tabs) {
// The debugger UI contains multiple nested JBTabsImpl, where the innermost JBTabsImpl has only one tab. In this case,
// the action should target the outer JBTabsImpl.
if (tabs == null || tabs != myTabs) {
return null;
}
if (isNavigatable(tabs)) {
return tabs;
}
Component c = tabs.getParent();
while (c != null) {
if (c instanceof JBTabsImpl && isNavigatable((JBTabsImpl)c)) {
return (JBTabsImpl)c;
}
c = c.getParent();
}
return null;
}
private static boolean isNavigatable(JBTabsImpl tabs) {
final int selectedIndex = tabs.myVisibleInfos.indexOf(tabs.getSelectedInfo());
return tabs.isNavigationVisible() && selectedIndex >= 0 && tabs.myNavigationActionsEnabled;
}
public void reconnect(String actionId) {
myShadow.reconnect(myActionManager.getAction(actionId));
}
protected abstract void _update(AnActionEvent e, final JBTabsImpl tabs, int selectedIndex);
@Override
public final void actionPerformed(final AnActionEvent e) {
JBTabsImpl tabs = e.getData(NAVIGATION_ACTIONS_KEY);
tabs = findNavigatableTabs(tabs);
if (tabs == null) return;
final int index = tabs.myVisibleInfos.indexOf(tabs.getSelectedInfo());
if (index == -1) return;
_actionPerformed(e, tabs, index);
}
protected abstract void _actionPerformed(final AnActionEvent e, final JBTabsImpl tabs, final int selectedIndex);
}
private static class SelectNextAction extends BaseNavigationAction {
private SelectNextAction(JBTabsImpl tabs, @NotNull ActionManager mgr) {
super(IdeActions.ACTION_NEXT_TAB, tabs, mgr);
}
@Override
protected void _update(final AnActionEvent e, final JBTabsImpl tabs, int selectedIndex) {
e.getPresentation().setEnabled(tabs.findEnabledForward(selectedIndex, true) != null);
}
@Override
protected void _actionPerformed(final AnActionEvent e, final JBTabsImpl tabs, final int selectedIndex) {
tabs.select(tabs.findEnabledForward(selectedIndex, true), true);
}
}
private static class SelectPreviousAction extends BaseNavigationAction {
private SelectPreviousAction(JBTabsImpl tabs, @NotNull ActionManager mgr) {
super(IdeActions.ACTION_PREVIOUS_TAB, tabs, mgr);
}
@Override
protected void _update(final AnActionEvent e, final JBTabsImpl tabs, int selectedIndex) {
e.getPresentation().setEnabled(tabs.findEnabledBackward(selectedIndex, true) != null);
}
@Override
protected void _actionPerformed(final AnActionEvent e, final JBTabsImpl tabs, final int selectedIndex) {
tabs.select(tabs.findEnabledBackward(selectedIndex, true), true);
}
}
private void disposePopupListener() {
if (myActivePopup != null) {
myActivePopup.removePopupMenuListener(myPopupListener);
myActivePopup = null;
}
}
@Override
public JBTabsPresentation setStealthTabMode(final boolean stealthTabMode) {
myStealthTabMode = stealthTabMode;
relayout(true, false);
return this;
}
public boolean isStealthTabMode() {
return myStealthTabMode;
}
@Override
public JBTabsPresentation setSideComponentVertical(final boolean vertical) {
myHorizontalSide = !vertical;
for (TabInfo each : myVisibleInfos) {
each.getChangeSupport().firePropertyChange(TabInfo.ACTION_GROUP, "new1", "new2");
}
relayout(true, false);
return this;
}
@Override
public JBTabsPresentation setSideComponentOnTabs(boolean onTabs) {
mySideComponentOnTabs = onTabs;
relayout(true, false);
return this;
}
@Override
public JBTabsPresentation setSingleRow(boolean singleRow) {
myLayout = singleRow ? mySingleRowLayout : myTableLayout;
relayout(true, false);
return this;
}
@Override
public JBTabsPresentation setGhostsAlwaysVisible(final boolean visible) {
myGhostsAlwaysVisible = visible;
relayout(true, false);
return this;
}
public boolean useSmallLabels() {
return false;
}
public boolean useBoldLabels() {
return false;
}
public boolean hasUnderline() {
return false;
}
public boolean isGhostsAlwaysVisible() {
return myGhostsAlwaysVisible;
}
@Override
public boolean isSingleRow() {
return getEffectiveLayout() == mySingleRowLayout;
}
public boolean isSideComponentVertical() {
return !myHorizontalSide;
}
public boolean isSideComponentOnTabs() {
return mySideComponentOnTabs;
}
public TabLayout getEffectiveLayout() {
if (myLayout == myTableLayout && getTabsPosition() == JBTabsPosition.top) return myTableLayout;
return mySingleRowLayout;
}
@Override
public JBTabsPresentation setUiDecorator(@Nullable UiDecorator decorator) {
myUiDecorator = decorator == null ? ourDefaultDecorator : decorator;
applyDecoration();
return this;
}
@Override
protected void setUI(final ComponentUI newUI) {
super.setUI(newUI);
applyDecoration();
}
@Override
public void updateUI() {
super.updateUI();
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
applyDecoration();
revalidateAndRepaint(false);
}
});
}
private void applyDecoration() {
if (myUiDecorator != null) {
UiDecorator.UiDecoration uiDecoration = myUiDecorator.getDecoration();
for (TabLabel each : myInfo2Label.values()) {
each.apply(uiDecoration);
}
}
for (TabInfo each : getTabs()) {
adjust(each);
}
relayout(true, false);
}
private void adjust(final TabInfo each) {
if (myAdjustBorders) {
UIUtil.removeScrollBorder(each.getComponent());
}
}
public void sortTabs(Comparator<TabInfo> comparator) {
Collections.sort(myVisibleInfos, comparator);
relayout(true, false);
}
private boolean isRequestFocusOnLastFocusedComponent() {
return myRequestFocusOnLastFocusedComponent;
}
@Override
public JBTabsPresentation setRequestFocusOnLastFocusedComponent(final boolean requestFocusOnLastFocusedComponent) {
myRequestFocusOnLastFocusedComponent = requestFocusOnLastFocusedComponent;
return this;
}
@Override
@Nullable
public Object getData(@NonNls final String dataId) {
if (myDataProvider != null) {
final Object value = myDataProvider.getData(dataId);
if (value != null) return value;
}
if (SwitchProvider.KEY.getName().equals(dataId) && myOwnSwitchProvider) {
return this;
}
if (QuickActionProvider.KEY.getName().equals(dataId)) {
return this;
}
return NAVIGATION_ACTIONS_KEY.is(dataId) ? this : null;
}
@Override
public List<AnAction> getActions(boolean originalProvider) {
ArrayList<AnAction> result = new ArrayList<AnAction>();
TabInfo selection = getSelectedInfo();
if (selection != null) {
ActionGroup group = selection.getGroup();
if (group != null) {
AnAction[] children = group.getChildren(null);
Collections.addAll(result, children);
}
}
return result;
}
@Override
public DataProvider getDataProvider() {
return myDataProvider;
}
public JBTabsImpl setDataProvider(@NotNull final DataProvider dataProvider) {
myDataProvider = dataProvider;
return this;
}
public static boolean isSelectionClick(final MouseEvent e, boolean canBeQuick) {
if (e.getClickCount() == 1 || canBeQuick) {
if (!e.isPopupTrigger()) {
return e.getButton() == MouseEvent.BUTTON1 && !e.isControlDown() && !e.isAltDown() && !e.isMetaDown();
}
}
return false;
}
private static class DefaultDecorator implements UiDecorator {
@Override
@NotNull
public UiDecoration getDecoration() {
return new UiDecoration(null, new Insets(0, 4, 0, 5));
}
}
public Rectangle layout(JComponent c, Rectangle bounds) {
final Rectangle now = c.getBounds();
if (!bounds.equals(now)) {
c.setBounds(bounds);
}
c.putClientProperty(LAYOUT_DONE, Boolean.TRUE);
return bounds;
}
public Rectangle layout(JComponent c, int x, int y, int width, int height) {
return layout(c, new Rectangle(x, y, width, height));
}
public static void resetLayout(JComponent c) {
if (c == null) return;
c.putClientProperty(LAYOUT_DONE, null);
c.putClientProperty(STRETCHED_BY_WIDTH, null);
}
private void applyResetComponents() {
for (int i = 0; i < getComponentCount(); i++) {
final Component each = getComponent(i);
if (each instanceof JComponent) {
final JComponent jc = (JComponent)each;
final Object done = jc.getClientProperty(LAYOUT_DONE);
if (!Boolean.TRUE.equals(done)) {
layout(jc, new Rectangle(0, 0, 0, 0));
}
}
}
}
@Override
@NotNull
public JBTabsPresentation setTabLabelActionsMouseDeadzone(final TimedDeadzone.Length length) {
myTabActionsMouseDeadzone = length;
final List<TabInfo> all = getTabs();
for (TabInfo each : all) {
final TabLabel eachLabel = myInfo2Label.get(each);
eachLabel.updateTabActions();
}
return this;
}
@Override
@NotNull
public JBTabsPresentation setTabsPosition(final JBTabsPosition position) {
myPosition = position;
relayout(true, false);
return this;
}
@Override
public JBTabsPosition getTabsPosition() {
return myPosition;
}
public TimedDeadzone.Length getTabActionsMouseDeadzone() {
return myTabActionsMouseDeadzone;
}
@Override
public JBTabsPresentation setTabDraggingEnabled(boolean enabled) {
myTabDraggingEnabled = enabled;
return this;
}
public boolean isTabDraggingEnabled() {
return myTabDraggingEnabled;
}
@Override
public JBTabsPresentation setProvideSwitchTargets(boolean provide) {
myOwnSwitchProvider = provide;
return this;
}
void reallocate(TabInfo source, TabInfo target) {
if (source == target || source == null || target == null) return;
final int targetIndex = myVisibleInfos.indexOf(target);
myVisibleInfos.remove(source);
myVisibleInfos.add(targetIndex, source);
invalidate();
relayout(true, true);
}
boolean isHorizontalTabs() {
return getTabsPosition() == JBTabsPosition.top || getTabsPosition() == JBTabsPosition.bottom;
}
@Override
public void putInfo(@NotNull Map<String, String> info) {
final TabInfo selected = getSelectedInfo();
if (selected != null) {
selected.putInfo(info);
}
}
public void setUseBufferedPaint(boolean useBufferedPaint) {
myUseBufferedPaint = useBufferedPaint;
revalidate();
repaint();
}
@Override
public List<SwitchTarget> getTargets(boolean onlyVisible, boolean originalProvider) {
ArrayList<SwitchTarget> result = new ArrayList<SwitchTarget>();
for (TabInfo each : myVisibleInfos) {
result.add(new TabTarget(each));
}
if (originalProvider && mySwitchDelegate != null) {
List<SwitchTarget> additional = mySwitchDelegate.getTargets(onlyVisible, false);
if (additional != null) {
result.addAll(additional);
}
}
return result;
}
@Override
public SwitchTarget getCurrentTarget() {
if (mySwitchDelegate != null) {
SwitchTarget selection = mySwitchDelegate.getCurrentTarget();
if (selection != null) return selection;
}
return new TabTarget(getSelectedInfo());
}
private class TabTarget extends ComparableObject.Impl implements SwitchTarget {
private final TabInfo myInfo;
private TabTarget(TabInfo info) {
myInfo = info;
}
@Override
public ActionCallback switchTo(boolean requestFocus) {
return select(myInfo, requestFocus);
}
@Override
public boolean isVisible() {
return getRectangle() != null;
}
@Override
public RelativeRectangle getRectangle() {
TabLabel label = myInfo2Label.get(myInfo);
if (label.getRootPane() == null) return null;
Rectangle b = label.getBounds();
b.x += 2;
b.width -= 4;
b.y += 2;
b.height -= 4;
return new RelativeRectangle(label.getParent(), b);
}
@Override
public Component getComponent() {
return myInfo2Label.get(myInfo);
}
@Override
public String toString() {
return myInfo.getText();
}
@NotNull
@Override
public Object[] getEqualityObjects() {
return new Object[]{myInfo};
}
}
@Override
public void resetDropOver(TabInfo tabInfo) {
if (myDropInfo != null) {
TabInfo dropInfo = myDropInfo;
myDropInfo = null;
myShowDropLocation = true;
setDropInfoIndex(-1);
if (!isDisposed()) {
removeTab(dropInfo, null, false, true);
}
}
}
@Override
public Image startDropOver(TabInfo tabInfo, RelativePoint point) {
myDropInfo = tabInfo;
int index = myLayout.getDropIndexFor(point.getPoint(this));
setDropInfoIndex(index);
addTab(myDropInfo, index, true, true);
TabLabel label = myInfo2Label.get(myDropInfo);
Dimension size = label.getPreferredSize();
label.setBounds(0, 0, size.width, size.height);
BufferedImage img = UIUtil.createImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = img.createGraphics();
label.paintOffscreen(g);
g.dispose();
relayout(true, false);
return img;
}
@Override
public void processDropOver(TabInfo over, RelativePoint point) {
int index = myLayout.getDropIndexFor(point.getPoint(this));
if (index != getDropInfoIndex()) {
setDropInfoIndex(index);
relayout(true, false);
}
}
public int getDropInfoIndex() {
return myDropInfoIndex;
}
public boolean isEmptyVisible() {
return myVisibleInfos.isEmpty();
}
public int getInterTabSpaceLength() {
return 1;
}
@Override
public String toString() {
return "JBTabs visible=" + myVisibleInfos + " selected=" + mySelectedInfo;
}
}