IDEA-127739 Navigation Tab
[idea/community.git] / platform / platform-impl / src / com / intellij / ui / content / impl / ContentManagerImpl.java
1 /*
2  * Copyright 2000-2014 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.content.impl;
17
18 import com.intellij.ide.DataManager;
19 import com.intellij.openapi.Disposable;
20 import com.intellij.openapi.actionSystem.AnAction;
21 import com.intellij.openapi.actionSystem.DataProvider;
22 import com.intellij.openapi.actionSystem.PlatformDataKeys;
23 import com.intellij.openapi.application.ApplicationManager;
24 import com.intellij.openapi.components.ServiceManager;
25 import com.intellij.openapi.diagnostic.Logger;
26 import com.intellij.openapi.project.Project;
27 import com.intellij.openapi.util.*;
28 import com.intellij.openapi.wm.FocusCommand;
29 import com.intellij.openapi.wm.IdeFocusManager;
30 import com.intellij.openapi.wm.ex.IdeFocusTraversalPolicy;
31 import com.intellij.ui.components.panels.NonOpaquePanel;
32 import com.intellij.ui.components.panels.Wrapper;
33 import com.intellij.ui.content.*;
34 import com.intellij.ui.switcher.SwitchProvider;
35 import com.intellij.ui.switcher.SwitchTarget;
36 import com.intellij.util.SmartList;
37 import com.intellij.util.containers.ContainerUtil;
38 import com.intellij.util.ui.UIUtil;
39 import org.jetbrains.annotations.NonNls;
40 import org.jetbrains.annotations.NotNull;
41 import org.jetbrains.annotations.Nullable;
42
43 import javax.swing.*;
44 import java.awt.*;
45 import java.beans.PropertyChangeEvent;
46 import java.beans.PropertyChangeListener;
47 import java.util.ArrayList;
48 import java.util.HashSet;
49 import java.util.List;
50 import java.util.Set;
51
52 /**
53  * @author Anton Katilin
54  * @author Vladimir Kondratyev
55  */
56 public class ContentManagerImpl implements ContentManager, PropertyChangeListener, Disposable.Parent {
57   private static final Logger LOG = Logger.getInstance("#com.intellij.ui.content.impl.ContentManagerImpl");
58
59   private ContentUI myUI;
60   private final List<Content> myContents = new ArrayList<Content>();
61   private final List<ContentManagerListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
62   private final List<Content> mySelection = new ArrayList<Content>();
63   private final boolean myCanCloseContents;
64
65   private Wrapper.FocusHolder myFocusProxy;
66   private MyNonOpaquePanel myComponent;
67
68   private final Set<Content> myContentWithChangedComponent = new HashSet<Content>();
69
70   private boolean myDisposed;
71   private final Project myProject;
72
73   private final List<DataProvider> dataProviders = new SmartList<DataProvider>();
74
75   /**
76    * WARNING: as this class adds listener to the ProjectManager which is removed on projectClosed event, all instances of this class
77    * must be created on already OPENED projects, otherwise there will be memory leak!
78    */
79   public ContentManagerImpl(@NotNull ContentUI contentUI, boolean canCloseContents, @NotNull Project project) {
80     myProject = project;
81     myCanCloseContents = canCloseContents;
82     myUI = contentUI;
83     myUI.setManager(this);
84
85     Disposer.register(project, this);
86     Disposer.register(this, contentUI);
87   }
88
89   @Override
90   public boolean canCloseContents() {
91     return myCanCloseContents;
92   }
93
94   @NotNull
95   @Override
96   public JComponent getComponent() {
97     if (myComponent == null) {
98       myComponent = new MyNonOpaquePanel();
99
100       myFocusProxy = new Wrapper.FocusHolder();
101       myFocusProxy.setOpaque(false);
102       myFocusProxy.setPreferredSize(new Dimension(0, 0));
103
104       MyContentComponent contentComponent = new MyContentComponent();
105       contentComponent.setContent(myUI.getComponent());
106       contentComponent.setFocusCycleRoot(true);
107
108       myComponent.add(myFocusProxy, BorderLayout.NORTH);
109       myComponent.add(contentComponent, BorderLayout.CENTER);
110     }
111     return myComponent;
112   }
113
114   @NotNull
115   @Override
116   public ActionCallback getReady(@NotNull Object requestor) {
117     Content selected = getSelectedContent();
118     if (selected == null) return new ActionCallback.Done();
119     BusyObject busyObject = selected.getBusyObject();
120     return busyObject != null ? busyObject.getReady(requestor) : new ActionCallback.Done();
121   }
122
123   private class MyNonOpaquePanel extends NonOpaquePanel implements DataProvider {
124     public MyNonOpaquePanel() {
125       super(new BorderLayout());
126     }
127
128     @Override
129     @Nullable
130     public Object getData(@NonNls String dataId) {
131       if (PlatformDataKeys.CONTENT_MANAGER.is(dataId) || PlatformDataKeys.NONEMPTY_CONTENT_MANAGER.is(dataId) && getContentCount() > 1) {
132         return ContentManagerImpl.this;
133       }
134
135       for (DataProvider dataProvider : dataProviders) {
136         Object data = dataProvider.getData(dataId);
137         if (data != null) {
138           return data;
139         }
140       }
141
142       if (myUI instanceof DataProvider) {
143         return ((DataProvider)myUI).getData(dataId);
144       }
145
146       DataProvider provider = DataManager.getDataProvider(this);
147       return provider == null ? null : provider.getData(dataId);
148     }
149   }
150
151   private class MyContentComponent extends NonOpaquePanel implements SwitchProvider {
152     @Override
153     public List<SwitchTarget> getTargets(boolean onlyVisible, boolean originalProvider) {
154       if (myUI instanceof SwitchProvider) {
155         return ((SwitchProvider)myUI).getTargets(onlyVisible, false);
156       }
157       return new SmartList<SwitchTarget>();
158     }
159
160     @Override
161     public SwitchTarget getCurrentTarget() {
162       return myUI instanceof SwitchProvider ? ((SwitchProvider)myUI).getCurrentTarget() : null;
163     }
164
165     @Override
166     public JComponent getComponent() {
167       return myUI instanceof SwitchProvider ? myUI.getComponent() : this;
168     }
169
170     @Override
171     public boolean isCycleRoot() {
172       return myUI instanceof SwitchProvider && ((SwitchProvider)myUI).isCycleRoot();
173     }
174   }
175
176   @Override
177   public void addContent(@NotNull Content content, final int order) {
178     doAddContent(content, order);
179   }
180
181   @Override
182   public void addContent(@NotNull Content content) {
183     doAddContent(content, -1);
184   }
185
186   @Override
187   public void addContent(@NotNull final Content content, final Object constraints) {
188     doAddContent(content, -1);
189   }
190
191   private void doAddContent(@NotNull final Content content, final int index) {
192     ApplicationManager.getApplication().assertIsDispatchThread();
193     if (myContents.contains(content)) {
194       myContents.remove(content);
195       myContents.add(index == -1 ? myContents.size() : index, content);
196       return;
197     }
198
199     ((ContentImpl)content).setManager(this);
200     final int insertIndex = index == -1 ? myContents.size() : index;
201     myContents.add(insertIndex, content);
202     content.addPropertyChangeListener(this);
203     fireContentAdded(content, insertIndex, ContentManagerEvent.ContentOperation.add);
204     if (myUI.isToSelectAddedContent() || mySelection.isEmpty() && !myUI.canBeEmptySelection()) {
205       if (myUI.isSingleSelection()) {
206         setSelectedContent(content);
207       }
208       else {
209         addSelectedContent(content);
210       }
211     }
212
213     Disposer.register(this, content);
214   }
215
216   @Override
217   public boolean removeContent(@NotNull Content content, final boolean dispose) {
218     return removeContent(content, true, dispose).isDone();
219   }
220
221   @NotNull
222   @Override
223   public ActionCallback removeContent(@NotNull Content content, boolean dispose, final boolean trackFocus, final boolean forcedFocus) {
224     final ActionCallback result = new ActionCallback();
225     removeContent(content, true, dispose).doWhenDone(new Runnable() {
226       @Override
227       public void run() {
228         if (trackFocus) {
229           Content current = getSelectedContent();
230           if (current != null) {
231             setSelectedContent(current, true, true, !forcedFocus);
232           }
233           else {
234             result.setDone();
235           }
236         }
237         else {
238           result.setDone();
239         }
240       }
241     });
242
243     return result;
244   }
245
246   @NotNull
247   private ActionCallback removeContent(@NotNull Content content, boolean trackSelection, boolean dispose) {
248     ApplicationManager.getApplication().assertIsDispatchThread();
249     int indexToBeRemoved = getIndexOfContent(content);
250     if (indexToBeRemoved == -1) return new ActionCallback.Rejected();
251
252     try {
253       Content selection = mySelection.isEmpty() ? null : mySelection.get(mySelection.size() - 1);
254       int selectedIndex = selection != null ? myContents.indexOf(selection) : -1;
255
256       if (!fireContentRemoveQuery(content, indexToBeRemoved, ContentManagerEvent.ContentOperation.undefined)) {
257         return new ActionCallback.Rejected();
258       }
259       if (!content.isValid()) {
260         return new ActionCallback.Rejected();
261       }
262
263       boolean wasSelected = isSelected(content);
264       if (wasSelected) {
265         removeFromSelection(content);
266       }
267
268       int indexToSelect = -1;
269       if (wasSelected) {
270         int i = indexToBeRemoved - 1;
271         if (i >= 0) {
272           indexToSelect = i;
273         }
274         else if (getContentCount() > 1) {
275           indexToSelect = 0;
276         }
277       }
278       else if (selectedIndex > indexToBeRemoved) {
279         indexToSelect = selectedIndex - 1;
280       }
281
282       myContents.remove(content);
283       content.removePropertyChangeListener(this);
284
285       fireContentRemoved(content, indexToBeRemoved, ContentManagerEvent.ContentOperation.remove);
286       ((ContentImpl)content).setManager(null);
287
288
289       if (dispose) {
290         Disposer.dispose(content);
291       }
292
293       int newSize = myContents.size();
294
295       ActionCallback result = new ActionCallback();
296
297       if (newSize > 0 && trackSelection) {
298         if (indexToSelect > -1) {
299           final Content toSelect = myContents.get(indexToSelect);
300           if (!isSelected(toSelect)) {
301             if (myUI.isSingleSelection()) {
302               setSelectedContentCB(toSelect).notify(result);
303             }
304             else {
305               addSelectedContent(toSelect);
306               result.setDone();
307             }
308           }
309         }
310       }
311       else {
312         mySelection.clear();
313       }
314
315       return result;
316     }
317     finally {
318       if (ApplicationManager.getApplication().isDispatchThread()) {
319         myUI.getComponent().updateUI(); //cleanup visibleComponent from Alloy...TabbedPaneUI
320       }
321     }
322   }
323
324   @Override
325   public void removeAllContents(final boolean dispose) {
326     Content[] contents = getContents();
327     for (Content content : contents) {
328       removeContent(content, dispose);
329     }
330   }
331
332   @Override
333   public int getContentCount() {
334     return myContents.size();
335   }
336
337   @Override
338   @NotNull
339   public Content[] getContents() {
340     return myContents.toArray(new Content[myContents.size()]);
341   }
342
343   //TODO[anton,vova] is this method needed?
344   @Override
345   public Content findContent(String displayName) {
346     for (Content content : myContents) {
347       if (content.getDisplayName().equals(displayName)) {
348         return content;
349       }
350     }
351     return null;
352   }
353
354   @Override
355   public Content getContent(int index) {
356     return index >= 0 && index < myContents.size() ? myContents.get(index) : null;
357   }
358
359   @Override
360   public Content getContent(JComponent component) {
361     Content[] contents = getContents();
362     for (Content content : contents) {
363       if (Comparing.equal(component, content.getComponent())) {
364         return content;
365       }
366     }
367     return null;
368   }
369
370   @Override
371   public int getIndexOfContent(Content content) {
372     return myContents.indexOf(content);
373   }
374
375   @NotNull
376   @Override
377   public String getCloseActionName() {
378     return myUI.getCloseActionName();
379   }
380
381   @NotNull
382   @Override
383   public String getCloseAllButThisActionName() {
384     return myUI.getCloseAllButThisActionName();
385   }
386
387   @NotNull
388   @Override
389   public String getPreviousContentActionName() {
390     return myUI.getPreviousContentActionName();
391   }
392
393   @NotNull
394   @Override
395   public String getNextContentActionName() {
396     return myUI.getNextContentActionName();
397   }
398
399   @Override
400   public List<AnAction> getAdditionalPopupActions(@NotNull final Content content) {
401     return null;
402   }
403
404   @Override
405   public boolean canCloseAllContents() {
406     if (!canCloseContents()) {
407       return false;
408     }
409     for (Content content : myContents) {
410       if (content.isCloseable()) {
411         return true;
412       }
413     }
414     return false;
415   }
416
417   @Override
418   public void addSelectedContent(@NotNull final Content content) {
419     if (!checkSelectionChangeShouldBeProcessed(content, false)) return;
420
421     if (getIndexOfContent(content) == -1) {
422       throw new IllegalArgumentException("content not found: " + content);
423     }
424     if (!isSelected(content)) {
425       mySelection.add(content);
426       fireSelectionChanged(content, ContentManagerEvent.ContentOperation.add);
427     }
428   }
429
430   private boolean checkSelectionChangeShouldBeProcessed(Content content, boolean implicit) {
431     if (!myUI.canChangeSelectionTo(content, implicit)) {
432       return false;
433     }
434
435     final boolean result = !isSelected(content) || myContentWithChangedComponent.contains(content);
436     myContentWithChangedComponent.remove(content);
437
438     return result;
439   }
440
441   @Override
442   public void removeFromSelection(@NotNull Content content) {
443     if (!isSelected(content)) return;
444     mySelection.remove(content);
445     fireSelectionChanged(content, ContentManagerEvent.ContentOperation.remove);
446   }
447
448   @Override
449   public boolean isSelected(@NotNull Content content) {
450     return mySelection.contains(content);
451   }
452
453   @Override
454   @NotNull
455   public Content[] getSelectedContents() {
456     return mySelection.toArray(new Content[mySelection.size()]);
457   }
458
459   @Override
460   @Nullable
461   public Content getSelectedContent() {
462     return mySelection.isEmpty() ? null : mySelection.get(0);
463   }
464
465   @Override
466   public void setSelectedContent(@NotNull Content content, boolean requestFocus) {
467     setSelectedContentCB(content, requestFocus);
468   }
469
470   @NotNull
471   @Override
472   public ActionCallback setSelectedContentCB(@NotNull final Content content, final boolean requestFocus) {
473     return setSelectedContentCB(content, requestFocus, true);
474   }
475
476   @Override
477   public void setSelectedContent(@NotNull Content content, boolean requestFocus, boolean forcedFocus) {
478     setSelectedContentCB(content, requestFocus, forcedFocus);
479   }
480
481   @NotNull
482   @Override
483   public ActionCallback setSelectedContentCB(@NotNull final Content content, final boolean requestFocus, final boolean forcedFocus) {
484     return setSelectedContent(content, requestFocus, forcedFocus, false);
485   }
486
487   @NotNull
488   @Override
489   public ActionCallback setSelectedContent(@NotNull final Content content, final boolean requestFocus, final boolean forcedFocus, boolean implicit) {
490     if (isSelected(content) && requestFocus) {
491       return requestFocus(content, forcedFocus);
492     }
493
494     if (!checkSelectionChangeShouldBeProcessed(content, implicit)) {
495       return new ActionCallback.Rejected();
496     }
497     if (!myContents.contains(content)) {
498       throw new IllegalArgumentException("Cannot find content:" + content.getDisplayName());
499     }
500
501     final boolean focused = isSelectionHoldsFocus();
502
503     final Content[] old = getSelectedContents();
504
505     final ActiveRunnable selection = new ActiveRunnable() {
506       @NotNull
507       @Override
508       public ActionCallback run() {
509         if (myDisposed || getIndexOfContent(content) == -1) return new ActionCallback.Rejected();
510
511         for (Content each : old) {
512           removeFromSelection(each);
513         }
514
515         addSelectedContent(content);
516
517         if (requestFocus) {
518           return requestFocus(content, forcedFocus);
519         }
520         return new ActionCallback.Done();
521       }
522     };
523
524     final ActionCallback result = new ActionCallback();
525     boolean enabledFocus = getFocusManager().isFocusTransferEnabled();
526     if (focused || requestFocus) {
527       if (enabledFocus) {
528         return getFocusManager().requestFocus(myFocusProxy, true).doWhenProcessed(new Runnable() {
529           @Override
530           public void run() {
531             selection.run().notify(result);
532           }
533         });
534       }
535       return selection.run().notify(result);
536     }
537     else {
538       return selection.run().notify(result);
539     }
540   }
541
542   private boolean isSelectionHoldsFocus() {
543     boolean focused = false;
544     final Content[] selection = getSelectedContents();
545     for (Content each : selection) {
546       if (UIUtil.isFocusAncestor(each.getComponent())) {
547         focused = true;
548         break;
549       }
550     }
551     return focused;
552   }
553
554   @NotNull
555   @Override
556   public ActionCallback setSelectedContentCB(@NotNull Content content) {
557     return setSelectedContentCB(content, false);
558   }
559
560   @Override
561   public void setSelectedContent(@NotNull final Content content) {
562     setSelectedContentCB(content);
563   }
564
565   @Override
566   public ActionCallback selectPreviousContent() {
567     int contentCount = getContentCount();
568     LOG.assertTrue(contentCount > 1);
569     Content selectedContent = getSelectedContent();
570     int index = getIndexOfContent(selectedContent);
571     index = (index - 1 + contentCount) % contentCount;
572     final Content content = getContent(index);
573     if (content == null) {
574       return null;
575     }
576     return setSelectedContentCB(content, true);
577   }
578
579   @Override
580   public ActionCallback selectNextContent() {
581     int contentCount = getContentCount();
582     LOG.assertTrue(contentCount > 1);
583     Content selectedContent = getSelectedContent();
584     int index = getIndexOfContent(selectedContent);
585     index = (index + 1) % contentCount;
586     final Content content = getContent(index);
587     if (content == null) {
588       return null;
589     }
590     return setSelectedContentCB(content, true);
591   }
592
593   @Override
594   public void addContentManagerListener(@NotNull ContentManagerListener l) {
595     myListeners.add(0,l);
596   }
597
598   @Override
599   public void removeContentManagerListener(@NotNull ContentManagerListener l) {
600     myListeners.remove(l);
601   }
602
603
604   private void fireContentAdded(Content content, int newIndex, ContentManagerEvent.ContentOperation operation) {
605     ContentManagerEvent event = new ContentManagerEvent(this, content, newIndex, operation);
606     for (ContentManagerListener listener : myListeners) {
607       listener.contentAdded(event);
608     }
609   }
610
611   private void fireContentRemoved(Content content, int oldIndex, ContentManagerEvent.ContentOperation operation) {
612     ContentManagerEvent event = new ContentManagerEvent(this, content, oldIndex, operation);
613     for (ContentManagerListener listener : myListeners) {
614       listener.contentRemoved(event);
615     }
616   }
617
618   private void fireSelectionChanged(Content content, ContentManagerEvent.ContentOperation operation) {
619     ContentManagerEvent event = new ContentManagerEvent(this, content, myContents.indexOf(content), operation);
620     for (ContentManagerListener listener : myListeners) {
621       listener.selectionChanged(event);
622     }
623   }
624
625   private boolean fireContentRemoveQuery(Content content, int oldIndex, ContentManagerEvent.ContentOperation operation) {
626     ContentManagerEvent event = new ContentManagerEvent(this, content, oldIndex, operation);
627     for (ContentManagerListener listener : myListeners) {
628       listener.contentRemoveQuery(event);
629       if (event.isConsumed()) {
630         return false;
631       }
632     }
633     return true;
634   }
635
636   @NotNull
637   @Override
638   public ActionCallback requestFocus(final Content content, final boolean forced) {
639     final Content toSelect = content == null ? getSelectedContent() : content;
640     if (toSelect == null) return new ActionCallback.Rejected();
641     assert myContents.contains(toSelect);
642
643
644     return getFocusManager().requestFocus(new FocusCommand(content, toSelect.getPreferredFocusableComponent()) {
645       @NotNull
646       @Override
647       public ActionCallback run() {
648         return doRequestFocus(toSelect);
649       }
650     }, forced);
651   }
652
653   private IdeFocusManager getFocusManager() {
654     return IdeFocusManager.getInstance(myProject);
655   }
656
657   private static ActionCallback doRequestFocus(final Content toSelect) {
658     JComponent toFocus = computeWillFocusComponent(toSelect);
659
660     if (toFocus != null) {
661       toFocus.requestFocus();
662     }
663
664     return new ActionCallback.Done();
665   }
666
667   private static JComponent computeWillFocusComponent(Content toSelect) {
668     JComponent toFocus = toSelect.getPreferredFocusableComponent();
669     if (toFocus != null) {
670       toFocus = IdeFocusTraversalPolicy.getPreferredFocusedComponent(toFocus);
671     }
672
673     if (toFocus == null) toFocus = toSelect.getPreferredFocusableComponent();
674     return toFocus;
675   }
676
677   @Override
678   public void addDataProvider(@NotNull final DataProvider provider) {
679     dataProviders.add(provider);
680   }
681
682   @Override
683   public void propertyChange(@NotNull PropertyChangeEvent event) {
684     if (Content.PROP_COMPONENT.equals(event.getPropertyName())) {
685       myContentWithChangedComponent.add((Content)event.getSource());
686     }
687   }
688
689   @Override
690   @NotNull
691   public ContentFactory getFactory() {
692     return ServiceManager.getService(ContentFactory.class);
693   }
694
695   @Override
696   public void beforeTreeDispose() {
697     myUI.beforeDispose();
698   }
699
700   @Override
701   public void dispose() {
702     myDisposed = true;
703
704     myContents.clear();
705     mySelection.clear();
706     myContentWithChangedComponent.clear();
707     myUI = null;
708     myListeners.clear();
709     dataProviders.clear();
710   }
711
712   @Override
713   public boolean isDisposed() {
714     return myDisposed;
715   }
716
717   @Override
718   public boolean isSingleSelection() {
719     return myUI.isSingleSelection();
720   }
721 }