| /* |
| * Copyright 2000-2009 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; |
| |
| import com.intellij.openapi.actionSystem.ActionManager; |
| import com.intellij.openapi.actionSystem.AnAction; |
| import com.intellij.openapi.actionSystem.AnActionEvent; |
| import com.intellij.openapi.actionSystem.IdeActions; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.ui.components.JBTabbedPane; |
| import org.intellij.lang.annotations.JdkConstants; |
| import org.jetbrains.annotations.NonNls; |
| |
| import javax.swing.*; |
| import javax.swing.event.ChangeEvent; |
| import javax.swing.event.ChangeListener; |
| import javax.swing.plaf.TabbedPaneUI; |
| import javax.swing.plaf.basic.BasicTabbedPaneUI; |
| import java.awt.*; |
| import java.awt.event.MouseAdapter; |
| import java.awt.event.MouseEvent; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Method; |
| |
| public class TabbedPaneImpl extends JBTabbedPane implements TabbedPane { |
| |
| public static final PrevNextActionsDescriptor DEFAULT_PREV_NEXT_SHORTCUTS = new PrevNextActionsDescriptor(IdeActions.ACTION_NEXT_TAB, |
| IdeActions.ACTION_PREVIOUS_TAB); |
| |
| private static final Logger LOG = Logger.getInstance("#com.intellij.ui.TabbedPaneImpl"); |
| |
| private ScrollableTabSupport myScrollableTabSupport; |
| private AnAction myNextTabAction = null; |
| private AnAction myPreviousTabAction = null; |
| public PrevNextActionsDescriptor myInstallKeyboardNavigation = null; |
| |
| public TabbedPaneImpl(@JdkConstants.TabPlacement int tabPlacement) { |
| super(tabPlacement); |
| setFocusable(false); |
| addMouseListener( |
| new MouseAdapter() { |
| @Override |
| public void mousePressed(final MouseEvent e) { |
| _requestDefaultFocus(); |
| } |
| } |
| ); |
| } |
| |
| @Override |
| public void setKeyboardNavigation(PrevNextActionsDescriptor installKeyboardNavigation) { |
| myInstallKeyboardNavigation = installKeyboardNavigation; |
| } |
| |
| @Override |
| public JComponent getComponent() { |
| return this; |
| } |
| |
| @Override |
| public void addNotify() { |
| super.addNotify(); |
| if (myInstallKeyboardNavigation != null) { |
| installKeyboardNavigation(myInstallKeyboardNavigation); |
| } |
| } |
| |
| @Override |
| public void removeNotify() { |
| super.removeNotify(); |
| if (myInstallKeyboardNavigation != null) { |
| uninstallKeyboardNavigation(); |
| } |
| } |
| |
| @SuppressWarnings({"NonStaticInitializer"}) |
| private void installKeyboardNavigation(final PrevNextActionsDescriptor installKeyboardNavigation){ |
| myNextTabAction = new AnAction() { |
| { |
| setEnabledInModalContext(true); |
| } |
| |
| @Override |
| public void actionPerformed(final AnActionEvent e) { |
| int index = getSelectedIndex() + 1; |
| if (index >= getTabCount()) { |
| index = 0; |
| } |
| setSelectedIndex(index); |
| } |
| }; |
| final AnAction nextAction = ActionManager.getInstance().getAction(installKeyboardNavigation.getNextActionId()); |
| LOG.assertTrue(nextAction != null, "Cannot find action with specified id: " + installKeyboardNavigation.getNextActionId()); |
| myNextTabAction.registerCustomShortcutSet(nextAction.getShortcutSet(), this); |
| |
| myPreviousTabAction = new AnAction() { |
| { |
| setEnabledInModalContext(true); |
| } |
| |
| @Override |
| public void actionPerformed(final AnActionEvent e) { |
| int index = getSelectedIndex() - 1; |
| if (index < 0) { |
| index = getTabCount() - 1; |
| } |
| setSelectedIndex(index); |
| } |
| }; |
| final AnAction prevAction = ActionManager.getInstance().getAction(installKeyboardNavigation.getPrevActionId()); |
| LOG.assertTrue(prevAction != null, "Cannot find action with specified id: " + installKeyboardNavigation.getPrevActionId()); |
| myPreviousTabAction.registerCustomShortcutSet(prevAction.getShortcutSet(), this); |
| } |
| |
| private void uninstallKeyboardNavigation() { |
| if (myNextTabAction != null) { |
| myNextTabAction.unregisterCustomShortcutSet(this); |
| myNextTabAction = null; |
| } |
| if (myPreviousTabAction != null) { |
| myPreviousTabAction.unregisterCustomShortcutSet(this); |
| myPreviousTabAction = null; |
| } |
| } |
| |
| |
| @Override |
| public void setUI(final TabbedPaneUI ui){ |
| super.setUI(ui); |
| if(ui instanceof BasicTabbedPaneUI){ |
| myScrollableTabSupport=new ScrollableTabSupport((BasicTabbedPaneUI)ui); |
| }else{ |
| myScrollableTabSupport=null; |
| } |
| } |
| |
| /** |
| * Scrolls tab to visible area. If tabbed pane has <code>JTabbedPane.WRAP_TAB_LAYOUT</code> layout policy then |
| * the method does nothing. |
| * @param index index of tab to be scrolled. |
| */ |
| @Override |
| public final void scrollTabToVisible(final int index){ |
| if(myScrollableTabSupport==null|| WRAP_TAB_LAYOUT==getTabLayoutPolicy()){ // tab scrolling isn't supported by UI |
| return; |
| } |
| final TabbedPaneUI tabbedPaneUI=getUI(); |
| Rectangle tabBounds=tabbedPaneUI.getTabBounds(this,index); |
| final int tabPlacement=getTabPlacement(); |
| if(TOP==tabPlacement || BOTTOM==tabPlacement){ //tabs are on the top or bottom |
| if(tabBounds.x<50){ //if tab is to the left of visible area |
| int leadingTabIndex=myScrollableTabSupport.getLeadingTabIndex(); |
| while(leadingTabIndex != index && leadingTabIndex>0 && tabBounds.x<50){ |
| myScrollableTabSupport.setLeadingTabIndex(leadingTabIndex-1); |
| leadingTabIndex=myScrollableTabSupport.getLeadingTabIndex(); |
| tabBounds=tabbedPaneUI.getTabBounds(this,index); |
| } |
| }else if(tabBounds.x+tabBounds.width>getWidth()-50){ // if tab's right side is out of visible range |
| int leadingTabIndex=myScrollableTabSupport.getLeadingTabIndex(); |
| while(leadingTabIndex != index && leadingTabIndex<getTabCount()-1 && tabBounds.x+tabBounds.width>getWidth()-50){ |
| myScrollableTabSupport.setLeadingTabIndex(leadingTabIndex+1); |
| leadingTabIndex=myScrollableTabSupport.getLeadingTabIndex(); |
| tabBounds=tabbedPaneUI.getTabBounds(this,index); |
| } |
| } |
| }else{ // tabs are on left or right side |
| if(tabBounds.y<30){ //tab is above visible area |
| int leadingTabIndex=myScrollableTabSupport.getLeadingTabIndex(); |
| while(leadingTabIndex != index && leadingTabIndex>0 && tabBounds.y<30){ |
| myScrollableTabSupport.setLeadingTabIndex(leadingTabIndex-1); |
| leadingTabIndex=myScrollableTabSupport.getLeadingTabIndex(); |
| tabBounds=tabbedPaneUI.getTabBounds(this,index); |
| } |
| } else if(tabBounds.y+tabBounds.height>getHeight()-30){ //tab is under visible area |
| int leadingTabIndex=myScrollableTabSupport.getLeadingTabIndex(); |
| while(leadingTabIndex != index && leadingTabIndex<getTabCount()-1 && tabBounds.y+tabBounds.height>getHeight()-30){ |
| myScrollableTabSupport.setLeadingTabIndex(leadingTabIndex+1); |
| leadingTabIndex=myScrollableTabSupport.getLeadingTabIndex(); |
| tabBounds=tabbedPaneUI.getTabBounds(this,index); |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void setSelectedIndex(final int index){ |
| if (index >= getTabCount()) return; |
| |
| try { |
| super.setSelectedIndex(index); |
| } |
| catch (ArrayIndexOutOfBoundsException e) { |
| //http://www.jetbrains.net/jira/browse/IDEADEV-22331 |
| //may happen on Mac OSX 10.5 |
| return; |
| } |
| |
| scrollTabToVisible(index); |
| doLayout(); |
| } |
| |
| //http://www.jetbrains.net/jira/browse/IDEADEV-22331 |
| //to let repaint happen since AIOBE is thrown from Mac OSX's UI |
| @Override |
| protected void fireStateChanged() { |
| // Guaranteed to return a non-null array |
| Object[] listeners = listenerList.getListenerList(); |
| // Process the listeners last to first, notifying |
| // those that are interested in this event |
| for (int i = listeners.length - 2; i >= 0; i -= 2) { |
| if (listeners[i] == ChangeListener.class) { |
| // Lazily create the event: |
| if (changeEvent == null) changeEvent = new ChangeEvent(this); |
| final ChangeListener each = (ChangeListener)listeners[i + 1]; |
| if (each != null && each.getClass().getName().indexOf("apple.laf.CUIAquaTabbedPane") >= 0) { |
| |
| SwingUtilities.invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| revalidate(); |
| repaint(); |
| } |
| }); |
| |
| continue; |
| } |
| |
| each.stateChanged(changeEvent); |
| } |
| } |
| } |
| |
| |
| @Override |
| public final void removeTabAt (final int index) { |
| super.removeTabAt (index); |
| //This event should be fired necessarily because when swing fires an event |
| // page to be removed is still in the tabbed pane. There can be a situation when |
| // event fired according to swing event contains invalid information about selected page. |
| fireStateChanged(); |
| } |
| |
| private void _requestDefaultFocus() { |
| final Component selectedComponent = getSelectedComponent(); |
| if (selectedComponent instanceof TabbedPaneWrapper.TabWrapper) { |
| ((TabbedPaneWrapper.TabWrapper)selectedComponent).requestDefaultFocus(); |
| } |
| else { |
| super.requestDefaultFocus(); |
| } |
| } |
| |
| protected final int getTabIndexAt(final int x,final int y){ |
| final TabbedPaneUI ui=getUI(); |
| for (int i = 0; i < getTabCount(); i++) { |
| final Rectangle bounds = ui.getTabBounds(this, i); |
| if (bounds.contains(x, y)) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| /** |
| * That is hack-helper for working with scrollable tab layout. The problem is BasicTabbedPaneUI doesn't |
| * have any API to scroll tab to visible area. Therefore we have to implement it... |
| */ |
| private final class ScrollableTabSupport{ |
| private final BasicTabbedPaneUI myUI; |
| @NonNls public static final String TAB_SCROLLER_NAME = "tabScroller"; |
| @NonNls public static final String LEADING_TAB_INDEX_NAME = "leadingTabIndex"; |
| @NonNls public static final String SET_LEADING_TAB_INDEX_METHOD = "setLeadingTabIndex"; |
| |
| public ScrollableTabSupport(final BasicTabbedPaneUI ui){ |
| myUI=ui; |
| } |
| |
| /** |
| * @return value of <code>leadingTabIndex</code> field of BasicTabbedPaneUI.ScrollableTabSupport class. |
| */ |
| public int getLeadingTabIndex() { |
| try { |
| final Field tabScrollerField = BasicTabbedPaneUI.class.getDeclaredField(TAB_SCROLLER_NAME); |
| tabScrollerField.setAccessible(true); |
| final Object tabScrollerValue = tabScrollerField.get(myUI); |
| |
| final Field leadingTabIndexField = tabScrollerValue.getClass().getDeclaredField(LEADING_TAB_INDEX_NAME); |
| leadingTabIndexField.setAccessible(true); |
| return leadingTabIndexField.getInt(tabScrollerValue); |
| } |
| catch (Exception exc) { |
| final String writer = StringUtil.getThrowableText(exc); |
| throw new IllegalStateException("myUI=" + myUI + "; cause=" + writer); |
| } |
| } |
| |
| public void setLeadingTabIndex(final int leadingIndex) { |
| try { |
| final Field tabScrollerField = BasicTabbedPaneUI.class.getDeclaredField(TAB_SCROLLER_NAME); |
| tabScrollerField.setAccessible(true); |
| final Object tabScrollerValue = tabScrollerField.get(myUI); |
| |
| Method setLeadingIndexMethod = null; |
| final Method[] methods = tabScrollerValue.getClass().getDeclaredMethods(); |
| for (final Method method : methods) { |
| if (SET_LEADING_TAB_INDEX_METHOD.equals(method.getName())) { |
| setLeadingIndexMethod = method; |
| break; |
| } |
| } |
| if (setLeadingIndexMethod == null) { |
| throw new IllegalStateException("method setLeadingTabIndex not found"); |
| } |
| setLeadingIndexMethod.setAccessible(true); |
| setLeadingIndexMethod.invoke(tabScrollerValue, Integer.valueOf(getTabPlacement()), Integer.valueOf(leadingIndex)); |
| } |
| catch (Exception exc) { |
| final String writer = StringUtil.getThrowableText(exc); |
| throw new IllegalStateException("myUI=" + myUI + "; cause=" + writer); |
| } |
| } |
| } |
| |
| @Override |
| public boolean isDisposed() { |
| return false; |
| } |
| } |