emptyIterator() used; misc warning fixes
[idea/community.git] / platform / lang-impl / src / com / intellij / ide / todo / TodoPanel.java
1 // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.ide.todo;
3
4 import com.intellij.find.FindModel;
5 import com.intellij.find.impl.FindInProjectUtil;
6 import com.intellij.icons.AllIcons;
7 import com.intellij.ide.*;
8 import com.intellij.ide.actions.NextOccurenceToolbarAction;
9 import com.intellij.ide.actions.PreviousOccurenceToolbarAction;
10 import com.intellij.ide.todo.nodes.TodoFileNode;
11 import com.intellij.ide.todo.nodes.TodoItemNode;
12 import com.intellij.ide.todo.nodes.TodoTreeHelper;
13 import com.intellij.ide.util.PsiNavigationSupport;
14 import com.intellij.ide.util.treeView.NodeDescriptor;
15 import com.intellij.openapi.Disposable;
16 import com.intellij.openapi.actionSystem.*;
17 import com.intellij.openapi.application.ApplicationManager;
18 import com.intellij.openapi.application.ModalityState;
19 import com.intellij.openapi.diagnostic.Logger;
20 import com.intellij.openapi.editor.Document;
21 import com.intellij.openapi.editor.RangeMarker;
22 import com.intellij.openapi.project.DumbService;
23 import com.intellij.openapi.project.Project;
24 import com.intellij.openapi.ui.SimpleToolWindowPanel;
25 import com.intellij.openapi.ui.Splitter;
26 import com.intellij.openapi.ui.popup.JBPopupFactory;
27 import com.intellij.openapi.util.Disposer;
28 import com.intellij.openapi.vfs.VirtualFile;
29 import com.intellij.openapi.wm.impl.VisibilityWatcher;
30 import com.intellij.psi.PsiDocumentManager;
31 import com.intellij.psi.PsiElement;
32 import com.intellij.psi.PsiFile;
33 import com.intellij.ui.*;
34 import com.intellij.ui.content.Content;
35 import com.intellij.ui.tree.AsyncTreeModel;
36 import com.intellij.ui.tree.StructureTreeModel;
37 import com.intellij.ui.treeStructure.Tree;
38 import com.intellij.usageView.UsageInfo;
39 import com.intellij.usages.impl.UsagePreviewPanel;
40 import com.intellij.util.Alarm;
41 import com.intellij.util.EditSourceOnDoubleClickHandler;
42 import com.intellij.util.OpenSourceUtil;
43 import com.intellij.util.PlatformIcons;
44 import com.intellij.util.ui.UIUtil;
45 import com.intellij.util.ui.tree.TreeModelAdapter;
46 import com.intellij.util.ui.tree.TreeUtil;
47 import org.jetbrains.annotations.NotNull;
48 import org.jetbrains.annotations.Nullable;
49
50 import javax.swing.*;
51 import javax.swing.event.TreeModelEvent;
52 import javax.swing.event.TreeSelectionEvent;
53 import javax.swing.event.TreeSelectionListener;
54 import javax.swing.tree.DefaultMutableTreeNode;
55 import javax.swing.tree.DefaultTreeModel;
56 import javax.swing.tree.TreeNode;
57 import javax.swing.tree.TreePath;
58 import java.awt.*;
59 import java.awt.event.KeyAdapter;
60 import java.awt.event.KeyEvent;
61 import java.util.ArrayList;
62 import java.util.HashSet;
63 import java.util.List;
64 import java.util.Set;
65
66 abstract class TodoPanel extends SimpleToolWindowPanel implements OccurenceNavigator, DataProvider, Disposable {
67   protected static final Logger LOG = Logger.getInstance(TodoPanel.class);
68
69   protected Project myProject;
70   private final TodoPanelSettings mySettings;
71   private final boolean myCurrentFileMode;
72   private final Content myContent;
73
74   private final Tree myTree;
75   private final MyTreeExpander myTreeExpander;
76   private final MyOccurenceNavigator myOccurenceNavigator;
77   protected final TodoTreeBuilder myTodoTreeBuilder;
78   private MyVisibilityWatcher myVisibilityWatcher;
79   private UsagePreviewPanel myUsagePreviewPanel;
80   private MyAutoScrollToSourceHandler myAutoScrollToSourceHandler;
81
82   public static final DataKey<TodoPanel> TODO_PANEL_DATA_KEY = DataKey.create("TodoPanel");
83
84   /**
85    * @param currentFileMode if {@code true} then view doesn't have "Group By Packages" and "Flatten Packages"
86    *                        actions.
87    */
88   TodoPanel(Project project, TodoPanelSettings settings, boolean currentFileMode, Content content) {
89     super(false, true);
90
91     myProject = project;
92     mySettings = settings;
93     myCurrentFileMode = currentFileMode;
94     myContent = content;
95
96     DefaultTreeModel model = new DefaultTreeModel(new DefaultMutableTreeNode());
97     myTree = new Tree(model);
98     myTreeExpander = new MyTreeExpander();
99     myOccurenceNavigator = new MyOccurenceNavigator();
100     initUI();
101     myTodoTreeBuilder = setupTreeStructure();
102     updateTodoFilter();
103     myTodoTreeBuilder.setShowPackages(mySettings.arePackagesShown);
104     myTodoTreeBuilder.setShowModules(mySettings.areModulesShown);
105     myTodoTreeBuilder.setFlattenPackages(mySettings.areFlattenPackages);
106
107     myVisibilityWatcher = new MyVisibilityWatcher();
108     myVisibilityWatcher.install(this);
109   }
110
111   private TodoTreeBuilder setupTreeStructure() {
112     TodoTreeBuilder todoTreeBuilder = createTreeBuilder(myTree, myProject);
113     TodoTreeStructure structure = todoTreeBuilder.getTodoTreeStructure();
114     StructureTreeModel structureTreeModel = new StructureTreeModel(structure, TodoTreeBuilder.NODE_DESCRIPTOR_COMPARATOR);
115     AsyncTreeModel asyncTreeModel = new AsyncTreeModel(structureTreeModel, myProject);
116     myTree.setModel(asyncTreeModel);
117     asyncTreeModel.addTreeModelListener(new MyExpandListener(todoTreeBuilder));
118     todoTreeBuilder.setModel(structureTreeModel);
119     Object selectableElement = structure.getFirstSelectableElement();
120     if (selectableElement != null) {
121       todoTreeBuilder.select(selectableElement);
122     }
123     return todoTreeBuilder;
124   }
125
126     public static class GroupByActionGroup extends DefaultActionGroup {
127     {
128       getTemplatePresentation().setIcon(AllIcons.Actions.GroupBy);
129       getTemplatePresentation().setText("View Options");
130       setPopup(true);
131     }
132
133     @Override
134     public void actionPerformed(@NotNull AnActionEvent e) {
135       JBPopupFactory.getInstance().createActionGroupPopup(null, this, e.getDataContext(), JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, true)
136                     .showUnderneathOf(e.getInputEvent().getComponent());
137     }
138   }
139
140   private class MyExpandListener extends TreeModelAdapter {
141
142     private final TodoTreeBuilder myBuilder;
143
144     MyExpandListener(TodoTreeBuilder builder) {
145       myBuilder = builder;
146     }
147
148     @Override
149     public void treeNodesInserted(TreeModelEvent e) {
150       TreePath parentPath = e.getTreePath();
151       if (parentPath == null || parentPath.getPathCount() > 2) return;
152       Object[] children = e.getChildren();
153       for (Object o : children) {
154         NodeDescriptor descriptor = TreeUtil.getUserObject(NodeDescriptor.class, o);
155         if (descriptor != null && myBuilder.isAutoExpandNode(descriptor)) {
156           ApplicationManager.getApplication().invokeLater(() -> {
157             if (myTree.isVisible(parentPath) && myTree.isExpanded(parentPath)) {
158               myTree.expandPath(parentPath.pathByAddingChild(o));
159             }
160           }, myBuilder.myProject.getDisposed());
161         }
162       }
163     }
164   }
165   
166   protected abstract TodoTreeBuilder createTreeBuilder(JTree tree, Project project);
167
168   private void initUI() {
169     UIUtil.setLineStyleAngled(myTree);
170     myTree.setShowsRootHandles(true);
171     myTree.setRootVisible(false);
172     myTree.setRowHeight(0); // enable variable-height rows
173     myTree.setCellRenderer(new TodoCompositeRenderer());
174     EditSourceOnDoubleClickHandler.install(myTree);
175     new TreeSpeedSearch(myTree);
176
177     DefaultActionGroup group = new DefaultActionGroup();
178     group.add(ActionManager.getInstance().getAction(IdeActions.ACTION_EDIT_SOURCE));
179     group.addSeparator();
180     group.add(CommonActionsManager.getInstance().createExpandAllAction(myTreeExpander, this));
181     group.add(CommonActionsManager.getInstance().createCollapseAllAction(myTreeExpander, this));
182     group.addSeparator();
183     group.add(ActionManager.getInstance().getAction(IdeActions.GROUP_VERSION_CONTROLS));
184     PopupHandler.installPopupHandler(myTree, group, ActionPlaces.TODO_VIEW_POPUP, ActionManager.getInstance());
185
186     myTree.addKeyListener(
187       new KeyAdapter() {
188         @Override
189         public void keyPressed(KeyEvent e) {
190           if (!e.isConsumed() && KeyEvent.VK_ENTER == e.getKeyCode()) {
191             TreePath path = myTree.getSelectionPath();
192             if (path == null) {
193               return;
194             }
195             final Object userObject = ((DefaultMutableTreeNode)path.getLastPathComponent()).getUserObject();
196             if (!((userObject instanceof NodeDescriptor ? (NodeDescriptor)userObject : null) instanceof TodoItemNode)) {
197               return;
198             }
199             OpenSourceUtil.openSourcesFrom(DataManager.getInstance().getDataContext(TodoPanel.this), false);
200           }
201         }
202       }
203     );
204
205
206     myUsagePreviewPanel = new UsagePreviewPanel(myProject, FindInProjectUtil.setupViewPresentation(false, new FindModel()));
207     Disposer.register(this, myUsagePreviewPanel);
208     myUsagePreviewPanel.setVisible(mySettings.showPreview);
209
210     setContent(createCenterComponent());
211
212     myTree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() {
213       @Override
214       public void valueChanged(final TreeSelectionEvent e) {
215         ApplicationManager.getApplication().invokeLater(() -> {
216           if (myUsagePreviewPanel.isVisible()) {
217             updatePreviewPanel();
218           }
219         }, ModalityState.NON_MODAL, myProject.getDisposed());
220       }
221     });
222
223     myAutoScrollToSourceHandler = new MyAutoScrollToSourceHandler();
224     myAutoScrollToSourceHandler.install(myTree);
225
226     // Create tool bars and register custom shortcuts
227
228     JPanel toolBarPanel = new JPanel(new GridLayout());
229
230     DefaultActionGroup toolbarGroup = new DefaultActionGroup();
231     toolbarGroup.add(new PreviousOccurenceToolbarAction(myOccurenceNavigator));
232     toolbarGroup.add(new NextOccurenceToolbarAction(myOccurenceNavigator));
233     toolbarGroup.add(new SetTodoFilterAction(myProject, mySettings, todoFilter -> setTodoFilter(todoFilter)));
234     toolbarGroup.add(createAutoScrollToSourceAction());
235
236     if (!myCurrentFileMode) {
237       DefaultActionGroup groupBy = createGroupByActionGroup();
238       toolbarGroup.add(groupBy);
239     }
240
241     toolbarGroup.add(new MyPreviewAction());
242     toolBarPanel.add(ActionManager.getInstance().createActionToolbar(ActionPlaces.TODO_VIEW_TOOLBAR, toolbarGroup, false).getComponent());
243
244     setToolbar(toolBarPanel);
245   }
246
247   @NotNull
248   protected DefaultActionGroup createGroupByActionGroup() {
249     ActionManager actionManager = ActionManager.getInstance();
250     return (DefaultActionGroup) actionManager.getAction("TodoViewGroupByGroup");
251   }
252
253   protected AnAction createAutoScrollToSourceAction() {
254     return myAutoScrollToSourceHandler.createToggleAction();
255   }
256
257   protected JComponent createCenterComponent() {
258     Splitter splitter = new OnePixelSplitter(false);
259     splitter.setSecondComponent(myUsagePreviewPanel);
260     splitter.setFirstComponent(ScrollPaneFactory.createScrollPane(myTree));
261     return splitter;
262   }
263
264   private void updatePreviewPanel() {
265     if (myProject == null || myProject.isDisposed()) return;
266     List<UsageInfo> infos = new ArrayList<>();
267     final TreePath path = myTree.getSelectionPath();
268     if (path != null) {
269       DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
270       Object userObject = node.getUserObject();
271       if (userObject instanceof NodeDescriptor) {
272         Object element = ((NodeDescriptor)userObject).getElement();
273         TodoItemNode pointer = myTodoTreeBuilder.getFirstPointerForElement(element);
274         if (pointer != null) {
275           final SmartTodoItemPointer value = pointer.getValue();
276           final Document document = value.getDocument();
277           final PsiFile psiFile = PsiDocumentManager.getInstance(myProject).getPsiFile(document);
278           final RangeMarker rangeMarker = value.getRangeMarker();
279           if (psiFile != null) {
280             infos.add(new UsageInfo(psiFile, rangeMarker.getStartOffset(), rangeMarker.getEndOffset()));
281             for (RangeMarker additionalMarker: value.getAdditionalRangeMarkers()) {
282               if (additionalMarker.isValid()) {
283                 infos.add(new UsageInfo(psiFile, additionalMarker.getStartOffset(), additionalMarker.getEndOffset()));
284               }
285             }
286           }
287         }
288       }
289     }
290     myUsagePreviewPanel.updateLayout(infos.isEmpty() ? null : infos);
291   }
292
293   @Override
294   public void dispose() {
295     if (myVisibilityWatcher != null) {
296       myVisibilityWatcher.deinstall(this);
297       myVisibilityWatcher = null;
298     }
299     myProject = null;
300   }
301
302   void rebuildCache() {
303     myTodoTreeBuilder.rebuildCache();
304   }
305
306   void rebuildCache(@NotNull Set<VirtualFile> files) {
307     myTodoTreeBuilder.rebuildCache(files);
308   }
309
310   /**
311    * Immediately updates tree.
312    */
313   void updateTree() {
314     myTodoTreeBuilder.updateTree();
315   }
316
317   /**
318    * Updates current filter. If previously set filter was removed then empty filter is set.
319    *
320    * @see TodoTreeBuilder#setTodoFilter
321    */
322   void updateTodoFilter() {
323     TodoFilter filter = TodoConfiguration.getInstance().getTodoFilter(mySettings.todoFilterName);
324     setTodoFilter(filter);
325   }
326
327   /**
328    * Sets specified {@code TodoFilter}. The method also updates window's title.
329    *
330    * @see TodoTreeBuilder#setTodoFilter
331    */
332   private void setTodoFilter(TodoFilter filter) {
333     // Clear name of current filter if it was removed from configuration.
334     String filterName = filter != null ? filter.getName() : null;
335     mySettings.todoFilterName = filterName;
336     // Update filter
337     myTodoTreeBuilder.setTodoFilter(filter);
338     // Update content's title
339     myContent.setDescription(filterName);
340   }
341
342   /**
343    * @return list of all selected virtual files.
344    */
345   @Nullable
346   protected PsiFile getSelectedFile() {
347     TreePath path = myTree.getSelectionPath();
348     if (path == null) {
349       return null;
350     }
351     DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
352     LOG.assertTrue(node != null);
353     if(node.getUserObject() == null){
354       return null;
355     }
356     return TodoTreeBuilder.getFileForNode(node);
357   }
358
359   protected void setDisplayName(String tabName) {
360     myContent.setDisplayName(tabName);
361   }
362
363   @Nullable
364   private PsiElement getSelectedElement() {
365     if (myTree == null) return null;
366     TreePath path = myTree.getSelectionPath();
367     if (path == null) {
368       return null;
369     }
370     DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
371     Object userObject = node.getUserObject();
372     final PsiElement selectedElement = TodoTreeHelper.getInstance(myProject).getSelectedElement(userObject);
373     if (selectedElement != null) return selectedElement;
374     return getSelectedFile();
375   }
376
377   @Override
378   public Object getData(@NotNull String dataId) {
379     if (CommonDataKeys.NAVIGATABLE.is(dataId)) {
380       TreePath path = myTree.getSelectionPath();
381       if (path == null) {
382         return null;
383       }
384       DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
385       Object userObject = node.getUserObject();
386       if (!(userObject instanceof NodeDescriptor)) {
387         return null;
388       }
389       Object element = ((NodeDescriptor)userObject).getElement();
390       if (!(element instanceof TodoFileNode || element instanceof TodoItemNode)) { // allow user to use F4 only on files an TODOs
391         return null;
392       }
393       TodoItemNode pointer = myTodoTreeBuilder.getFirstPointerForElement(element);
394       if (pointer != null) {
395         return PsiNavigationSupport.getInstance().createNavigatable(myProject,
396                                                                     pointer.getValue().getTodoItem().getFile()
397                                                                            .getVirtualFile(),
398                                                                     pointer.getValue().getRangeMarker()
399                                                                            .getStartOffset());
400       }
401       else {
402         return null;
403       }
404     }
405     else if (CommonDataKeys.VIRTUAL_FILE.is(dataId)) {
406       final PsiFile file = getSelectedFile();
407       return file != null ? file.getVirtualFile() : null;
408     }
409     else if (CommonDataKeys.PSI_ELEMENT.is(dataId)) {
410       return getSelectedElement();
411     }
412     else if (CommonDataKeys.VIRTUAL_FILE_ARRAY.is(dataId)) {
413       PsiFile file = getSelectedFile();
414       if (file != null) {
415         return new VirtualFile[]{file.getVirtualFile()};
416       }
417       else {
418         return VirtualFile.EMPTY_ARRAY;
419       }
420     }
421     else if (PlatformDataKeys.HELP_ID.is(dataId)) {
422       //noinspection HardCodedStringLiteral
423       return "find.todoList";
424     }
425     else if (TODO_PANEL_DATA_KEY.is(dataId)) {
426       return this;
427     }
428     return super.getData(dataId);
429   }
430
431   @Override
432   @Nullable
433   public OccurenceInfo goPreviousOccurence() {
434     return myOccurenceNavigator.goPreviousOccurence();
435   }
436
437   @NotNull
438   @Override
439   public String getNextOccurenceActionName() {
440     return myOccurenceNavigator.getNextOccurenceActionName();
441   }
442
443   @Override
444   @Nullable
445   public OccurenceInfo goNextOccurence() {
446     return myOccurenceNavigator.goNextOccurence();
447   }
448
449   @Override
450   public boolean hasNextOccurence() {
451     return myOccurenceNavigator.hasNextOccurence();
452   }
453
454   @NotNull
455   @Override
456   public String getPreviousOccurenceActionName() {
457     return myOccurenceNavigator.getPreviousOccurenceActionName();
458   }
459
460   @Override
461   public boolean hasPreviousOccurence() {
462     return myOccurenceNavigator.hasPreviousOccurence();
463   }
464
465   protected void rebuildWithAlarm(final Alarm alarm) {
466     alarm.cancelAllRequests();
467     alarm.addRequest(() -> {
468       final Set<VirtualFile> files = new HashSet<>();
469       DumbService.getInstance(myProject).runReadActionInSmartMode(() -> {
470         if (myTodoTreeBuilder.isDisposed()) return;
471         myTodoTreeBuilder.collectFiles(virtualFile -> {
472           files.add(virtualFile);
473           return true;
474         });
475         final Runnable runnable = () -> {
476           if (myTodoTreeBuilder.isDisposed()) return;
477           myTodoTreeBuilder.rebuildCache(files);
478           updateTree();
479         };
480         ApplicationManager.getApplication().invokeLater(runnable);
481       });
482     }, 300);
483   }
484
485   TreeExpander getTreeExpander() {
486     return myTreeExpander;
487   }
488   
489   private final class MyTreeExpander implements TreeExpander {
490     @Override
491     public boolean canCollapse() {
492       return true;
493     }
494
495     @Override
496     public boolean canExpand() {
497       return true;
498     }
499
500     @Override
501     public void collapseAll() {
502       TreeUtil.collapseAll(myTree, 0);
503     }
504
505     @Override
506     public void expandAll() {
507       TreeUtil.expandAll(myTree);
508     }
509   }
510
511   /**
512    * Provides support for "auto scroll to source" functionality
513    */
514   private final class MyAutoScrollToSourceHandler extends AutoScrollToSourceHandler {
515     MyAutoScrollToSourceHandler() {
516     }
517
518     @Override
519     protected boolean isAutoScrollMode() {
520       return mySettings.isAutoScrollToSource;
521     }
522
523     @Override
524     protected void setAutoScrollMode(boolean state) {
525       mySettings.isAutoScrollToSource = state;
526     }
527   }
528
529   /**
530    * Provides support for "Ctrl+Alt+Up/Down" navigation.
531    */
532   private final class MyOccurenceNavigator implements OccurenceNavigator {
533     @Override
534     public boolean hasNextOccurence() {
535       TreePath path = myTree.getSelectionPath();
536       if (path == null) {
537         return false;
538       }
539       DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
540       Object userObject = node.getUserObject();
541       if (userObject == null) {
542         return false;
543       }
544       if (userObject instanceof NodeDescriptor && ((NodeDescriptor)userObject).getElement() instanceof TodoItemNode) {
545         return myTree.getRowCount() != myTree.getRowForPath(path) + 1;
546       }
547       else {
548         return node.getChildCount() > 0;
549       }
550     }
551
552     @Override
553     public boolean hasPreviousOccurence() {
554       TreePath path = myTree.getSelectionPath();
555       if (path == null) {
556         return false;
557       }
558       DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
559       Object userObject = node.getUserObject();
560       return userObject instanceof NodeDescriptor && !isFirst(node);
561     }
562
563     private boolean isFirst(final TreeNode node) {
564       final TreeNode parent = node.getParent();
565       return parent == null || parent.getIndex(node) == 0 && isFirst(parent);
566     }
567
568     @Override
569     @Nullable
570     public OccurenceInfo goNextOccurence() {
571       return goToPointer(getNextPointer());
572     }
573
574     @Override
575     @Nullable
576     public OccurenceInfo goPreviousOccurence() {
577       return goToPointer(getPreviousPointer());
578     }
579
580     @NotNull
581     @Override
582     public String getNextOccurenceActionName() {
583       return IdeBundle.message("action.next.todo");
584     }
585
586     @NotNull
587     @Override
588     public String getPreviousOccurenceActionName() {
589       return IdeBundle.message("action.previous.todo");
590     }
591
592     @Nullable
593     private OccurenceInfo goToPointer(TodoItemNode pointer) {
594       if (pointer == null) return null;
595       myTodoTreeBuilder.select(pointer);
596       return new OccurenceInfo(
597         PsiNavigationSupport.getInstance()
598                             .createNavigatable(myProject, pointer.getValue().getTodoItem().getFile().getVirtualFile(),
599                                                pointer.getValue().getRangeMarker().getStartOffset()),
600         -1,
601         -1
602       );
603     }
604
605     @Nullable
606     private TodoItemNode getNextPointer() {
607       TreePath path = myTree.getSelectionPath();
608       if (path == null) {
609         return null;
610       }
611       DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
612       Object userObject = node.getUserObject();
613       if (!(userObject instanceof NodeDescriptor)) {
614         return null;
615       }
616       Object element = ((NodeDescriptor)userObject).getElement();
617       TodoItemNode pointer;
618       if (element instanceof TodoItemNode) {
619         pointer = myTodoTreeBuilder.getNextPointer((TodoItemNode)element);
620       }
621       else {
622         pointer = myTodoTreeBuilder.getFirstPointerForElement(element);
623       }
624       return pointer;
625     }
626
627     @Nullable
628     private TodoItemNode getPreviousPointer() {
629       TreePath path = myTree.getSelectionPath();
630       if (path == null) {
631         return null;
632       }
633       DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
634       Object userObject = node.getUserObject();
635       if (!(userObject instanceof NodeDescriptor)) {
636         return null;
637       }
638       Object element = ((NodeDescriptor)userObject).getElement();
639       TodoItemNode pointer;
640       if (element instanceof TodoItemNode) {
641         pointer = myTodoTreeBuilder.getPreviousPointer((TodoItemNode)element);
642       }
643       else {
644         Object sibling = myTodoTreeBuilder.getPreviousSibling(element);
645         if (sibling == null) {
646           return null;
647         }
648         pointer = myTodoTreeBuilder.getLastPointerForElement(sibling);
649       }
650       return pointer;
651     }
652   }
653
654   public static final class MyShowPackagesAction extends ToggleAction {
655     public MyShowPackagesAction() {
656       super(IdeBundle.message("action.group.by.packages"), null, PlatformIcons.GROUP_BY_PACKAGES);
657     }
658
659     @Override
660     public void update(@NotNull AnActionEvent e) {
661       e.getPresentation().setEnabled(e.getData(TODO_PANEL_DATA_KEY) != null);
662       super.update(e);
663     }
664
665     @Override
666     public boolean isSelected(@NotNull AnActionEvent e) {
667       TodoPanel todoPanel = e.getData(TODO_PANEL_DATA_KEY);
668       return todoPanel != null && todoPanel.mySettings.arePackagesShown;
669     }
670
671     @Override
672     public void setSelected(@NotNull AnActionEvent e, boolean state) {
673       TodoPanel todoPanel = e.getData(TODO_PANEL_DATA_KEY);
674       if (todoPanel != null) {
675         todoPanel.mySettings.arePackagesShown = state;
676         todoPanel.myTodoTreeBuilder.setShowPackages(state);
677       }
678     }
679   }
680
681   public static final class MyShowModulesAction extends ToggleAction {
682     public MyShowModulesAction() {
683       super(IdeBundle.message("action.group.by.modules"), null, AllIcons.Actions.GroupByModule);
684     }
685
686     @Override
687     public void update(@NotNull AnActionEvent e) {
688       e.getPresentation().setEnabled(e.getData(TODO_PANEL_DATA_KEY) != null);
689       super.update(e);
690     }
691
692     @Override
693     public boolean isSelected(@NotNull AnActionEvent e) {
694       TodoPanel todoPanel = e.getData(TODO_PANEL_DATA_KEY);
695       return todoPanel != null && todoPanel.mySettings.areModulesShown;
696     }
697
698     @Override
699     public void setSelected(@NotNull AnActionEvent e, boolean state) {
700       TodoPanel todoPanel = e.getData(TODO_PANEL_DATA_KEY);
701
702       if (todoPanel != null) {
703         todoPanel.mySettings.areModulesShown = state;
704         todoPanel.myTodoTreeBuilder.setShowModules(state);
705       }
706     }
707   }
708
709   public static final class MyFlattenPackagesAction extends ToggleAction {
710     public MyFlattenPackagesAction() {
711       super(IdeBundle.message("action.flatten.packages"), null, PlatformIcons.FLATTEN_PACKAGES_ICON);
712     }
713
714     @Override
715     public void update(@NotNull AnActionEvent e) {
716       super.update(e);
717       TodoPanel todoPanel = e.getData(TODO_PANEL_DATA_KEY);
718       e.getPresentation().setEnabled(todoPanel != null && todoPanel.mySettings.arePackagesShown);
719     }
720
721     @Override
722     public boolean isSelected(@NotNull AnActionEvent e) {
723       TodoPanel todoPanel = e.getData(TODO_PANEL_DATA_KEY);
724       return todoPanel != null && todoPanel.mySettings.areFlattenPackages;
725     }
726
727     @Override
728     public void setSelected(@NotNull AnActionEvent e, boolean state) {
729       TodoPanel todoPanel = e.getData(TODO_PANEL_DATA_KEY);
730       if (todoPanel != null) {
731         todoPanel.mySettings.areFlattenPackages = state;
732         todoPanel.myTodoTreeBuilder.setFlattenPackages(state);
733       }
734     }
735   }
736
737   private final class MyVisibilityWatcher extends VisibilityWatcher {
738     @Override
739     public void visibilityChanged() {
740       if (myProject.isOpen()) {
741         PsiDocumentManager.getInstance(myProject).performWhenAllCommitted(
742           () -> myTodoTreeBuilder.setUpdatable(isShowing()));
743       }
744     }
745   }
746
747   private final class MyPreviewAction extends ToggleAction {
748
749     MyPreviewAction() {
750       super("Preview Source", null, AllIcons.Actions.PreviewDetails);
751     }
752
753     @Override
754     public boolean isSelected(@NotNull AnActionEvent e) {
755       return mySettings.showPreview;
756     }
757
758     @Override
759     public void setSelected(@NotNull AnActionEvent e, boolean state) {
760       mySettings.showPreview = state;
761       myUsagePreviewPanel.setVisible(state);
762       if (state) {
763         updatePreviewPanel();
764       }
765     }
766   }
767 }