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