replaced <code></code> with more concise {@code}
[idea/community.git] / platform / platform-impl / src / com / intellij / ui / TabbedPaneWrapper.java
1 /*
2  * Copyright 2000-2017 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.Disposable;
19 import com.intellij.openapi.actionSystem.DataProvider;
20 import com.intellij.openapi.application.ex.ApplicationEx;
21 import com.intellij.openapi.application.ex.ApplicationManagerEx;
22 import com.intellij.openapi.project.Project;
23 import com.intellij.openapi.wm.IdeFocusManager;
24 import com.intellij.openapi.wm.ex.IdeFocusTraversalPolicy;
25 import com.intellij.ui.tabs.JBTabs;
26 import com.intellij.util.IJSwingUtilities;
27 import org.jetbrains.annotations.NotNull;
28 import org.jetbrains.annotations.Nullable;
29
30 import javax.swing.*;
31 import javax.swing.event.ChangeListener;
32 import java.awt.*;
33 import java.awt.event.MouseListener;
34
35 /**
36  * @author Anton Katilin
37  * @author Vladimir Kondratyev
38  */
39 public class TabbedPaneWrapper  {
40   protected TabbedPane myTabbedPane;
41   protected JComponent myTabbedPaneHolder;
42
43   private TabFactory myFactory;
44
45   protected TabbedPaneWrapper(boolean construct) {
46     if (construct) {
47       init(SwingConstants.TOP, TabbedPaneImpl.DEFAULT_PREV_NEXT_SHORTCUTS, new JTabbedPaneFactory(this));
48     }
49   }
50
51   public TabbedPaneWrapper(@NotNull Disposable parentDisposable) {
52     this(SwingConstants.TOP, TabbedPaneImpl.DEFAULT_PREV_NEXT_SHORTCUTS, parentDisposable);
53   }
54
55   /**
56    * Creates tabbed pane wrapper with specified tab placement
57    *
58    * @param tabPlacement tab placement. It one of the {@code SwingConstants.TOP},
59    * {@code SwingConstants.LEFT}, {@code SwingConstants.BOTTOM} or
60    * {@code SwingConstants.RIGHT}.
61    */
62   public TabbedPaneWrapper(int tabPlacement, PrevNextActionsDescriptor installKeyboardNavigation, @NotNull Disposable parentDisposable) {
63     final TabFactory factory;
64     if (SwingConstants.BOTTOM == tabPlacement || SwingConstants.TOP == tabPlacement) {
65       factory = new JBTabsFactory(this, null, parentDisposable);
66     } else {
67       factory = new JTabbedPaneFactory(this);
68     }
69
70     init(tabPlacement, installKeyboardNavigation, factory);
71   }
72
73   void init(int tabPlacement, PrevNextActionsDescriptor installKeyboardNavigation, TabFactory tabbedPaneFactory) {
74     myFactory = tabbedPaneFactory;
75
76     myTabbedPane = createTabbedPane(tabPlacement);
77     myTabbedPane.putClientProperty(TabbedPaneWrapper.class, myTabbedPane);
78     myTabbedPane.setKeyboardNavigation(installKeyboardNavigation);
79
80     myTabbedPaneHolder = createTabbedPaneHolder();
81     myTabbedPaneHolder.add(myTabbedPane.getComponent(), BorderLayout.CENTER);
82     myTabbedPaneHolder.setFocusTraversalPolicyProvider(true);
83     myTabbedPaneHolder.setFocusTraversalPolicy(new _MyFocusTraversalPolicy());
84
85     assertIsDispatchThread();
86   }
87
88   public boolean isDisposed() {
89     return myTabbedPane != null && myTabbedPane.isDisposed();
90   }
91
92   private void assertIsDispatchThread() {
93     final ApplicationEx application = ApplicationManagerEx.getApplicationEx();
94     if (application != null){
95       application.assertIsDispatchThread(myTabbedPane.getComponent());
96     }
97   }
98
99   public final void addChangeListener(final ChangeListener listener){
100     assertIsDispatchThread();
101     myTabbedPane.addChangeListener(listener);
102   }
103
104   public final void removeChangeListener(final ChangeListener listener){
105     assertIsDispatchThread();
106     myTabbedPane.removeChangeListener(listener);
107   }
108
109   protected TabbedPaneHolder createTabbedPaneHolder() {
110     return myFactory.createTabbedPaneHolder();
111   }
112
113   public final JComponent getComponent() {
114     assertIsDispatchThread();
115     return myTabbedPaneHolder;
116   }
117
118   /**
119    * @see javax.swing.JTabbedPane#addTab(java.lang.String, javax.swing.Icon, java.awt.Component, java.lang.String)
120    */
121   public final synchronized void addTab(final String title, final Icon icon, final JComponent component, final String tip) {
122     insertTab(title, icon, component, tip, myTabbedPane.getTabCount());
123   }
124
125   public final synchronized void addTab(final String title, final JComponent component) {
126     insertTab(title, null, component, null, myTabbedPane.getTabCount());
127   }
128
129   public synchronized void insertTab(final String title, final Icon icon, final JComponent component, final String tip, final int index) {
130     myTabbedPane.insertTab(title, icon, createTabWrapper(component), tip, index);
131   }
132
133   protected TabWrapper createTabWrapper(JComponent component) {
134     return myFactory.createTabWrapper(component);
135   }
136
137   protected TabbedPane createTabbedPane(final int tabPlacement) {
138     return myFactory.createTabbedPane(tabPlacement);
139   }
140
141   /**
142    * @see javax.swing.JTabbedPane#setTabPlacement
143    */
144   public final void setTabPlacement(final int tabPlacement) {
145     assertIsDispatchThread();
146     myTabbedPane.setTabPlacement(tabPlacement);
147   }
148
149   public final void addMouseListener(final MouseListener listener) {
150     assertIsDispatchThread();
151     myTabbedPane.addMouseListener(listener);
152   }
153
154   public final synchronized int getSelectedIndex() {
155     return myTabbedPane.getSelectedIndex();
156   }
157
158   /**
159    * @see javax.swing.JTabbedPane#getSelectedComponent()
160    */
161   public final synchronized JComponent getSelectedComponent() {
162     // Workaround for JDK 6 bug
163     final TabWrapper tabWrapper = myTabbedPane.getTabCount() > 0 ? (TabWrapper)myTabbedPane.getSelectedComponent():null;
164     return tabWrapper != null ? tabWrapper.getComponent() : null;
165   }
166
167   public final void setSelectedIndex(final int index) {
168     setSelectedIndex(index, true);
169   }
170
171   public final void setSelectedIndex(final int index, boolean requestFocus) {
172     assertIsDispatchThread();
173
174     final boolean hadFocus = IJSwingUtilities.hasFocus2(myTabbedPaneHolder);
175     myTabbedPane.setSelectedIndex(index);
176     if (hadFocus && requestFocus) {
177       IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> {
178         IdeFocusManager.getGlobalInstance().requestFocus(myTabbedPaneHolder, true);
179       });
180     }
181   }
182
183   public final void setSelectedComponent(final JComponent component){
184     assertIsDispatchThread();
185
186     final int index=indexOfComponent(component);
187     if(index==-1){
188       throw new IllegalArgumentException("component not found in tabbed pane wrapper");
189     }
190     setSelectedIndex(index);
191   }
192
193   public final synchronized void removeTabAt(final int index) {
194     assertIsDispatchThread();
195
196     final boolean hadFocus = IJSwingUtilities.hasFocus2(myTabbedPaneHolder);
197     final TabWrapper wrapper = getWrapperAt(index);
198     try {
199       myTabbedPane.removeTabAt(index);
200       if (myTabbedPane.getTabCount() == 0) {
201         // to clear BasicTabbedPaneUI.visibleComponent field
202         myTabbedPane.revalidate();
203       }
204       if (hadFocus) {
205         IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> {
206           IdeFocusManager.getGlobalInstance().requestFocus(myTabbedPaneHolder, true);
207         });
208       }
209     }
210     finally {
211       wrapper.dispose();
212     }
213   }
214
215   public final synchronized int getTabCount() {
216     return myTabbedPane.getTabCount();
217   }
218
219   public final Color getForegroundAt(final int index){
220     assertIsDispatchThread();
221     return myTabbedPane.getForegroundAt(index);
222   }
223
224   /**
225    * @see javax.swing.JTabbedPane#setForegroundAt(int, java.awt.Color)
226    */
227   public final void setForegroundAt(final int index,final Color color){
228     assertIsDispatchThread();
229     myTabbedPane.setForegroundAt(index,color);
230   }
231
232   public final Component getTabComponentAt(final int index) {
233     return myTabbedPane.getTabComponentAt(index);
234   }
235   /**
236    * @see javax.swing.JTabbedPane#setComponentAt(int, java.awt.Component)
237    */
238   public final synchronized JComponent getComponentAt(final int i) {
239     return getWrapperAt(i).getComponent();
240   }
241
242   private TabWrapper getWrapperAt(final int i) {
243     return (TabWrapper)myTabbedPane.getComponentAt(i);
244   }
245
246   public final void setTitleAt(final int index, final String title) {
247     assertIsDispatchThread();
248     myTabbedPane.setTitleAt(index, title);
249   }
250
251   public final void setToolTipTextAt(final int index, final String toolTipText) {
252     assertIsDispatchThread();
253     myTabbedPane.setToolTipTextAt(index, toolTipText);
254   }
255
256   /**
257    * @see javax.swing.JTabbedPane#setComponentAt(int, java.awt.Component)
258    */
259   public final synchronized void setComponentAt(final int index, final JComponent component) {
260     assertIsDispatchThread();
261     myTabbedPane.setComponentAt(index, createTabWrapper(component));
262   }
263
264   /**
265    * @see javax.swing.JTabbedPane#setIconAt(int, javax.swing.Icon)
266    */
267   public final void setIconAt(final int index, final Icon icon) {
268     assertIsDispatchThread();
269     myTabbedPane.setIconAt(index, icon);
270   }
271
272   public final void setEnabledAt(final int index, final boolean enabled) {
273     assertIsDispatchThread();
274     myTabbedPane.setEnabledAt(index, enabled);
275   }
276
277   /**
278    * @see javax.swing.JTabbedPane#indexOfComponent(java.awt.Component)
279    */
280   public final synchronized int indexOfComponent(final JComponent component) {
281     for (int i=0; i < myTabbedPane.getTabCount(); i++) {
282       final JComponent c = getWrapperAt(i).getComponent();
283       if (c == component) {
284         return i;
285       }
286     }
287     return -1;
288   }
289
290   /**
291    * @see javax.swing.JTabbedPane#getTabLayoutPolicy
292    */
293   public final synchronized int getTabLayoutPolicy(){
294     return myTabbedPane.getTabLayoutPolicy();
295   }
296
297   /**
298    * @see javax.swing.JTabbedPane#setTabLayoutPolicy
299    */
300   public final synchronized void setTabLayoutPolicy(final int policy){
301     myTabbedPane.setTabLayoutPolicy(policy);
302     final int index=myTabbedPane.getSelectedIndex();
303     if(index!=-1){
304       myTabbedPane.scrollTabToVisible(index);
305     }
306   }
307
308   /**
309    * @deprecated Keyboard navigation is installed/deinstalled automatically. This method does nothing now.
310    */
311   public final void installKeyboardNavigation(){
312   }
313
314   /**
315    * @deprecated Keyboard navigation is installed/deinstalled automatically. This method does nothing now.
316    */
317   public final void uninstallKeyboardNavigation(){
318   }
319
320   public final String getTitleAt(final int i) {
321     return myTabbedPane.getTitleAt(i);
322   }
323
324   public void setSelectedTitle(@Nullable final String title) {
325     if (title == null) return;
326
327     for (int i = 0; i < myTabbedPane.getTabCount(); i++) {
328       final String each = myTabbedPane.getTitleAt(i);
329       if (title.equals(each)) {
330         myTabbedPane.setSelectedIndex(i);
331         break;
332       }
333     }
334   }
335
336   @Nullable
337   public String getSelectedTitle() {
338     return getSelectedIndex() < 0 ? null : getTitleAt(getSelectedIndex());
339   }
340
341   public void removeAll() {
342     myTabbedPane.removeAll();
343   }
344
345   public static final class TabWrapper extends JPanel implements DataProvider{
346     private JComponent myComponent;
347
348     boolean myCustomFocus = true;
349
350     public TabWrapper(@NotNull final JComponent component) {
351       super(new BorderLayout());
352       myComponent = component;
353       add(component, BorderLayout.CENTER);
354     }
355
356     /*
357      * Make possible to search down for DataProviders
358      */
359     public Object getData(final String dataId) {
360       if(myComponent instanceof DataProvider){
361         return ((DataProvider)myComponent).getData(dataId);
362       } else {
363         return null;
364       }
365     }
366
367     public JComponent getComponent() {
368       return myComponent;
369     }
370
371     /**
372      * TabWrappers are never reused so we can fix the leak in some LAF's TabbedPane UI by cleanuping ourselves.
373      */
374     public void dispose() {
375       if (myComponent != null) {
376         remove(myComponent);
377         myComponent = null;
378       }
379     }
380
381     public boolean requestDefaultFocus() {
382       if (!myCustomFocus) return super.requestDefaultFocus();
383       if (myComponent == null) return false; // Just in case someone requests the focus when we're already removed from the Swing tree.
384       final JComponent preferredFocusedComponent = IdeFocusTraversalPolicy.getPreferredFocusedComponent(myComponent);
385       if (preferredFocusedComponent != null) {
386         if (!preferredFocusedComponent.requestFocusInWindow()) {
387           IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> {
388             IdeFocusManager.getGlobalInstance().requestFocus(preferredFocusedComponent, true);
389           });
390         }
391         return true;
392       } else {
393         return myComponent.requestDefaultFocus();
394       }
395     }
396
397     public void requestFocus() {
398       if (!myCustomFocus) {
399         IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> {
400           super.requestFocus();
401         });
402       } else {
403         requestDefaultFocus();
404       }
405     }
406
407     public boolean requestFocusInWindow() {
408       if (!myCustomFocus) return super.requestFocusInWindow();
409       return requestDefaultFocus();
410     }
411   }
412
413   private final class _MyFocusTraversalPolicy extends IdeFocusTraversalPolicy{
414     @Override
415     public boolean isNoDefaultComponent() {
416       return false;
417     }
418
419     public final Component getDefaultComponentImpl(final Container focusCycleRoot) {
420       final JComponent component=getSelectedComponent();
421       if(component!=null){
422         return IdeFocusTraversalPolicy.getPreferredFocusedComponent(component, this);
423       }else{
424         return null;
425       }
426     }
427   }
428
429   public static class TabbedPaneHolder extends JPanel {
430
431     private final TabbedPaneWrapper myWrapper;
432
433     protected TabbedPaneHolder(TabbedPaneWrapper wrapper) {
434       super(new BorderLayout());
435       myWrapper = wrapper;
436     }
437
438     public boolean requestDefaultFocus() {
439       final JComponent preferredFocusedComponent = IdeFocusTraversalPolicy.getPreferredFocusedComponent(myWrapper.myTabbedPane.getComponent());
440       if (preferredFocusedComponent != null) {
441         if (!preferredFocusedComponent.requestFocusInWindow()) {
442           IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> {
443             IdeFocusManager.getGlobalInstance().requestFocus(preferredFocusedComponent, true);
444           });
445         }
446         return true;
447       } else {
448         return super.requestDefaultFocus();
449       }
450     }
451
452     public final void requestFocus() {
453       requestDefaultFocus();
454     }
455
456     public final boolean requestFocusInWindow() {
457       return requestDefaultFocus();
458     }
459
460     public void updateUI() {
461       super.updateUI();
462       if (myWrapper != null) {
463         myWrapper.myTabbedPane.updateUI();
464       }
465     }
466
467     public TabbedPaneWrapper getTabbedPaneWrapper() {
468       return myWrapper;
469     }
470   }
471
472   public static TabbedPaneWrapper get(JTabbedPane tabs) {
473     return (TabbedPaneWrapper)tabs.getClientProperty(TabbedPaneWrapper.class);
474   }
475
476   private interface TabFactory {
477     TabbedPane createTabbedPane(int tabPlacement);
478     TabbedPaneHolder createTabbedPaneHolder();
479     TabWrapper createTabWrapper(JComponent component);
480   }
481
482   private static class JTabbedPaneFactory implements TabFactory {
483     private final TabbedPaneWrapper myWrapper;
484
485     private JTabbedPaneFactory(TabbedPaneWrapper wrapper) {
486       myWrapper = wrapper;
487     }
488
489     public TabbedPane createTabbedPane(int tabPlacement) {
490       return new TabbedPaneImpl(tabPlacement);
491     }
492
493     public TabbedPaneHolder createTabbedPaneHolder() {
494       return new TabbedPaneHolder(myWrapper);
495     }
496
497     public TabWrapper createTabWrapper(JComponent component) {
498       return new TabWrapper(component);
499     }
500   }
501
502   private static class JBTabsFactory implements TabFactory {
503
504     private final Project myProject;
505     private final Disposable myParent;
506     private final TabbedPaneWrapper myWrapper;
507
508     private JBTabsFactory(TabbedPaneWrapper wrapper, Project project, @NotNull Disposable parent) {
509       myWrapper = wrapper;
510       myProject = project;
511       myParent = parent;
512     }
513
514     public TabbedPane createTabbedPane(int tabPlacement) {
515       return new JBTabsPaneImpl(myProject, tabPlacement, myParent);
516     }
517
518     public TabbedPaneHolder createTabbedPaneHolder() {
519       return new TabbedPaneHolder(myWrapper) {
520         @Override
521         public boolean requestDefaultFocus() {
522           getTabs().requestFocus();
523           return true;
524         }
525
526       };
527     }
528
529     public TabWrapper createTabWrapper(JComponent component) {
530       final TabWrapper tabWrapper = new TabWrapper(component);
531       tabWrapper.myCustomFocus = false;
532       return tabWrapper;
533     }
534
535     public JBTabs getTabs() {
536       return ((JBTabsPaneImpl)myWrapper.myTabbedPane).getTabs();
537     }
538
539     public void dispose() {
540     }
541   }
542
543   public static class AsJBTabs extends TabbedPaneWrapper {
544     public AsJBTabs(@Nullable Project project, int tabPlacement, PrevNextActionsDescriptor installKeyboardNavigation, @NotNull Disposable parent) {
545       super(false);
546       init(tabPlacement, installKeyboardNavigation, new JBTabsFactory(this, project, parent));
547     }
548
549     public JBTabs getTabs() {
550       return ((JBTabsPaneImpl)myTabbedPane).getTabs();
551     }
552   }
553
554   public static class AsJTabbedPane extends TabbedPaneWrapper {
555     public AsJTabbedPane(int tabPlacement) {
556       super(false);
557       init(tabPlacement, TabbedPaneImpl.DEFAULT_PREV_NEXT_SHORTCUTS, new JTabbedPaneFactory(this));
558     }
559   }
560
561 }