replaced <code></code> with more concise {@code}
[idea/community.git] / platform / platform-impl / src / com / intellij / ui / TabbedPaneImpl.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.ui;
17
18 import com.intellij.openapi.actionSystem.ActionManager;
19 import com.intellij.openapi.actionSystem.AnAction;
20 import com.intellij.openapi.actionSystem.AnActionEvent;
21 import com.intellij.openapi.actionSystem.IdeActions;
22 import com.intellij.openapi.diagnostic.Logger;
23 import com.intellij.openapi.util.text.StringUtil;
24 import com.intellij.ui.components.JBTabbedPane;
25 import org.intellij.lang.annotations.JdkConstants;
26 import org.jetbrains.annotations.NonNls;
27
28 import javax.swing.*;
29 import javax.swing.event.ChangeEvent;
30 import javax.swing.event.ChangeListener;
31 import javax.swing.plaf.TabbedPaneUI;
32 import javax.swing.plaf.basic.BasicTabbedPaneUI;
33 import java.awt.*;
34 import java.awt.event.MouseAdapter;
35 import java.awt.event.MouseEvent;
36 import java.lang.reflect.Field;
37 import java.lang.reflect.Method;
38
39 public class TabbedPaneImpl extends JBTabbedPane implements TabbedPane {
40
41   public static final PrevNextActionsDescriptor DEFAULT_PREV_NEXT_SHORTCUTS = new PrevNextActionsDescriptor(IdeActions.ACTION_NEXT_TAB,
42                                                                                                             IdeActions.ACTION_PREVIOUS_TAB);
43
44   private static final Logger LOG = Logger.getInstance("#com.intellij.ui.TabbedPaneImpl");
45
46   private ScrollableTabSupport myScrollableTabSupport;
47   private AnAction myNextTabAction = null;
48   private AnAction myPreviousTabAction = null;
49   public PrevNextActionsDescriptor myInstallKeyboardNavigation = null;
50
51   public TabbedPaneImpl(@JdkConstants.TabPlacement int tabPlacement) {
52     super(tabPlacement);
53     setFocusable(false);
54     addMouseListener(
55       new MouseAdapter() {
56         @Override
57         public void mousePressed(final MouseEvent e) {
58           _requestDefaultFocus();
59         }
60       }
61     );
62   }
63
64   @Override
65   public void setKeyboardNavigation(PrevNextActionsDescriptor installKeyboardNavigation) {
66     myInstallKeyboardNavigation = installKeyboardNavigation;
67   }
68
69   @Override
70   public JComponent getComponent() {
71     return this;
72   }
73
74   @Override
75   public void addNotify() {
76     super.addNotify();
77     if (myInstallKeyboardNavigation != null) {
78       installKeyboardNavigation(myInstallKeyboardNavigation);
79     }
80   }
81
82   @Override
83   public void removeNotify() {
84     super.removeNotify();
85     if (myInstallKeyboardNavigation != null) {
86       uninstallKeyboardNavigation();
87     }
88   }
89
90   @SuppressWarnings({"NonStaticInitializer"})
91   private void installKeyboardNavigation(final PrevNextActionsDescriptor installKeyboardNavigation){
92     myNextTabAction = new AnAction() {
93       {
94         setEnabledInModalContext(true);
95       }
96
97       @Override
98       public void actionPerformed(final AnActionEvent e) {
99         int index = getSelectedIndex() + 1;
100         if (index >= getTabCount()) {
101           index = 0;
102         }
103         setSelectedIndex(index);
104       }
105     };
106     final AnAction nextAction = ActionManager.getInstance().getAction(installKeyboardNavigation.getNextActionId());
107     LOG.assertTrue(nextAction != null, "Cannot find action with specified id: " + installKeyboardNavigation.getNextActionId());
108     myNextTabAction.registerCustomShortcutSet(nextAction.getShortcutSet(), this);
109
110     myPreviousTabAction = new AnAction() {
111       {
112         setEnabledInModalContext(true);
113       }
114
115       @Override
116       public void actionPerformed(final AnActionEvent e) {
117         int index = getSelectedIndex() - 1;
118         if (index < 0) {
119           index = getTabCount() - 1;
120         }
121         setSelectedIndex(index);
122       }
123     };
124     final AnAction prevAction = ActionManager.getInstance().getAction(installKeyboardNavigation.getPrevActionId());
125     LOG.assertTrue(prevAction != null, "Cannot find action with specified id: " + installKeyboardNavigation.getPrevActionId());
126     myPreviousTabAction.registerCustomShortcutSet(prevAction.getShortcutSet(), this);
127   }
128
129   private void uninstallKeyboardNavigation() {
130     if (myNextTabAction != null) {
131       myNextTabAction.unregisterCustomShortcutSet(this);
132       myNextTabAction = null;
133     }
134     if (myPreviousTabAction != null) {
135       myPreviousTabAction.unregisterCustomShortcutSet(this);
136       myPreviousTabAction = null;
137     }
138   }
139
140
141   @Override
142   public void setUI(final TabbedPaneUI ui){
143     super.setUI(ui);
144     if(ui instanceof BasicTabbedPaneUI){
145       myScrollableTabSupport=new ScrollableTabSupport((BasicTabbedPaneUI)ui);
146     }else{
147       myScrollableTabSupport=null;
148     }
149   }
150
151   /**
152    * Scrolls tab to visible area. If tabbed pane has {@code JTabbedPane.WRAP_TAB_LAYOUT} layout policy then
153    * the method does nothing.
154    * @param index index of tab to be scrolled.
155    */
156   @Override
157   public final void scrollTabToVisible(final int index){
158     if(myScrollableTabSupport==null|| WRAP_TAB_LAYOUT==getTabLayoutPolicy()){ // tab scrolling isn't supported by UI
159       return;
160     }
161     final TabbedPaneUI tabbedPaneUI=getUI();
162     Rectangle tabBounds=tabbedPaneUI.getTabBounds(this,index);
163     final int tabPlacement=getTabPlacement();
164     if(TOP==tabPlacement || BOTTOM==tabPlacement){ //tabs are on the top or bottom
165       if(tabBounds.x<50){  //if tab is to the left of visible area
166         int leadingTabIndex=myScrollableTabSupport.getLeadingTabIndex();
167         while(leadingTabIndex != index && leadingTabIndex>0 && tabBounds.x<50){
168           myScrollableTabSupport.setLeadingTabIndex(leadingTabIndex-1);
169           leadingTabIndex=myScrollableTabSupport.getLeadingTabIndex();
170           tabBounds=tabbedPaneUI.getTabBounds(this,index);
171         }
172       }else if(tabBounds.x+tabBounds.width>getWidth()-50){ // if tab's right side is out of visible range
173         int leadingTabIndex=myScrollableTabSupport.getLeadingTabIndex();
174         while(leadingTabIndex != index && leadingTabIndex<getTabCount()-1 && tabBounds.x+tabBounds.width>getWidth()-50){
175           myScrollableTabSupport.setLeadingTabIndex(leadingTabIndex+1);
176           leadingTabIndex=myScrollableTabSupport.getLeadingTabIndex();
177           tabBounds=tabbedPaneUI.getTabBounds(this,index);
178         }
179       }
180     }else{ // tabs are on left or right side
181       if(tabBounds.y<30){ //tab is above visible area
182         int leadingTabIndex=myScrollableTabSupport.getLeadingTabIndex();
183         while(leadingTabIndex != index && leadingTabIndex>0 && tabBounds.y<30){
184           myScrollableTabSupport.setLeadingTabIndex(leadingTabIndex-1);
185           leadingTabIndex=myScrollableTabSupport.getLeadingTabIndex();
186           tabBounds=tabbedPaneUI.getTabBounds(this,index);
187         }
188       } else if(tabBounds.y+tabBounds.height>getHeight()-30){  //tab is under visible area
189         int leadingTabIndex=myScrollableTabSupport.getLeadingTabIndex();
190         while(leadingTabIndex != index && leadingTabIndex<getTabCount()-1 && tabBounds.y+tabBounds.height>getHeight()-30){
191           myScrollableTabSupport.setLeadingTabIndex(leadingTabIndex+1);
192           leadingTabIndex=myScrollableTabSupport.getLeadingTabIndex();
193           tabBounds=tabbedPaneUI.getTabBounds(this,index);
194         }
195       }
196     }
197   }
198
199   @Override
200   public void setSelectedIndex(final int index){
201     if (index >= getTabCount()) return;
202
203     try {
204       super.setSelectedIndex(index);
205     }
206     catch (ArrayIndexOutOfBoundsException e) {
207       //http://www.jetbrains.net/jira/browse/IDEADEV-22331
208       //may happen on Mac OSX 10.5
209       return;
210     }
211
212     scrollTabToVisible(index);
213     doLayout();
214   }
215
216  //http://www.jetbrains.net/jira/browse/IDEADEV-22331
217  //to let repaint happen since AIOBE is thrown from Mac OSX's UI
218  @Override
219  protected void fireStateChanged() {
220    // Guaranteed to return a non-null array
221    Object[] listeners = listenerList.getListenerList();
222    // Process the listeners last to first, notifying
223    // those that are interested in this event
224    for (int i = listeners.length - 2; i >= 0; i -= 2) {
225      if (listeners[i] == ChangeListener.class) {
226        // Lazily create the event:
227        if (changeEvent == null) changeEvent = new ChangeEvent(this);
228        final ChangeListener each = (ChangeListener)listeners[i + 1];
229        if (each != null) {
230          if (each.getClass().getName().contains("apple.laf.CUIAquaTabbedPane")) {
231
232            //noinspection SSBasedInspection
233            SwingUtilities.invokeLater(() -> {
234              revalidate();
235              repaint();
236            });
237
238            continue;
239          }
240
241          each.stateChanged(changeEvent);
242        }
243      }
244    }
245  }
246
247
248   @Override
249   public final void removeTabAt (final int index) {
250     super.removeTabAt (index);
251     //This event should be fired necessarily because when swing fires an event
252     // page to be removed is still in the tabbed pane. There can be a situation when
253     // event fired according to swing event contains invalid information about selected page.
254     fireStateChanged();
255   }
256
257   private void _requestDefaultFocus() {
258     final Component selectedComponent = getSelectedComponent();
259     if (selectedComponent instanceof TabbedPaneWrapper.TabWrapper) {
260       ((TabbedPaneWrapper.TabWrapper)selectedComponent).requestDefaultFocus();
261     }
262     else {
263       super.requestDefaultFocus();
264     }
265   }
266
267   protected final int getTabIndexAt(final int x,final int y){
268     final TabbedPaneUI ui=getUI();
269     for (int i = 0; i < getTabCount(); i++) {
270       final Rectangle bounds = ui.getTabBounds(this, i);
271       if (bounds.contains(x, y)) {
272         return i;
273       }
274     }
275     return -1;
276   }
277
278   /**
279    * That is hack-helper for working with scrollable tab layout. The problem is BasicTabbedPaneUI doesn't
280    * have any API to scroll tab to visible area. Therefore we have to implement it...
281    */
282   private final class ScrollableTabSupport{
283     private final BasicTabbedPaneUI myUI;
284     @NonNls public static final String TAB_SCROLLER_NAME = "tabScroller";
285     @NonNls public static final String LEADING_TAB_INDEX_NAME = "leadingTabIndex";
286     @NonNls public static final String SET_LEADING_TAB_INDEX_METHOD = "setLeadingTabIndex";
287
288     public ScrollableTabSupport(final BasicTabbedPaneUI ui){
289       myUI=ui;
290     }
291
292     /**
293      * @return value of {@code leadingTabIndex} field of BasicTabbedPaneUI.ScrollableTabSupport class.
294      */
295     public int getLeadingTabIndex() {
296       try {
297         final Field tabScrollerField = BasicTabbedPaneUI.class.getDeclaredField(TAB_SCROLLER_NAME);
298         tabScrollerField.setAccessible(true);
299         final Object tabScrollerValue = tabScrollerField.get(myUI);
300
301         final Field leadingTabIndexField = tabScrollerValue.getClass().getDeclaredField(LEADING_TAB_INDEX_NAME);
302         leadingTabIndexField.setAccessible(true);
303         return leadingTabIndexField.getInt(tabScrollerValue);
304       }
305       catch (Exception exc) {
306         final String writer = StringUtil.getThrowableText(exc);
307         throw new IllegalStateException("myUI=" + myUI + "; cause=" + writer);
308       }
309     }
310
311     public void setLeadingTabIndex(final int leadingIndex) {
312       try {
313         final Field tabScrollerField = BasicTabbedPaneUI.class.getDeclaredField(TAB_SCROLLER_NAME);
314         tabScrollerField.setAccessible(true);
315         final Object tabScrollerValue = tabScrollerField.get(myUI);
316
317         Method setLeadingIndexMethod = null;
318         final Method[] methods = tabScrollerValue.getClass().getDeclaredMethods();
319         for (final Method method : methods) {
320           if (SET_LEADING_TAB_INDEX_METHOD.equals(method.getName())) {
321             setLeadingIndexMethod = method;
322             break;
323           }
324         }
325         if (setLeadingIndexMethod == null) {
326           throw new IllegalStateException("method setLeadingTabIndex not found");
327         }
328         setLeadingIndexMethod.setAccessible(true);
329         setLeadingIndexMethod.invoke(tabScrollerValue, Integer.valueOf(getTabPlacement()), Integer.valueOf(leadingIndex));
330       }
331       catch (Exception exc) {
332         final String writer = StringUtil.getThrowableText(exc);
333         throw new IllegalStateException("myUI=" + myUI + "; cause=" + writer);
334       }
335     }
336   }
337
338   @Override
339   public boolean isDisposed() {
340     return false;
341   }
342 }