b36c35732c9887c4f22496233fe278ef2603a906
[idea/community.git] / plugins / git4idea / src / git4idea / ui / branch / dashboard / BranchesTree.kt
1 // Copyright 2000-2020 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 git4idea.ui.branch.dashboard
3
4 import com.intellij.dvcs.DvcsUtil
5 import com.intellij.dvcs.branch.GroupingKey
6 import com.intellij.dvcs.branch.isGroupingEnabled
7 import com.intellij.icons.AllIcons
8 import com.intellij.ide.dnd.TransferableList
9 import com.intellij.ide.dnd.aware.DnDAwareTree
10 import com.intellij.ide.util.treeView.TreeState
11 import com.intellij.openapi.actionSystem.ActionManager
12 import com.intellij.openapi.application.runInEdt
13 import com.intellij.openapi.components.*
14 import com.intellij.openapi.project.Project
15 import com.intellij.openapi.util.TextRange
16 import com.intellij.openapi.util.text.StringUtil
17 import com.intellij.psi.codeStyle.FixingLayoutMatcher
18 import com.intellij.psi.codeStyle.MinusculeMatcher
19 import com.intellij.psi.codeStyle.NameUtil
20 import com.intellij.ui.*
21 import com.intellij.ui.speedSearch.SpeedSearch
22 import com.intellij.ui.speedSearch.SpeedSearchSupply
23 import com.intellij.util.EditSourceOnDoubleClickHandler.isToggleEvent
24 import com.intellij.util.PlatformIcons
25 import com.intellij.util.ThreeState
26 import com.intellij.util.containers.SmartHashSet
27 import com.intellij.util.ui.UIUtil
28 import com.intellij.util.ui.tree.TreeUtil
29 import com.intellij.vcs.log.util.VcsLogUtil
30 import git4idea.config.GitVcsSettings
31 import git4idea.repo.GitRepositoryManager
32 import git4idea.ui.branch.dashboard.BranchesDashboardActions.BranchesTreeActionGroup
33 import icons.DvcsImplIcons
34 import java.awt.GraphicsEnvironment
35 import java.awt.datatransfer.Transferable
36 import java.awt.event.MouseEvent
37 import javax.swing.JComponent
38 import javax.swing.JTree
39 import javax.swing.TransferHandler
40 import javax.swing.event.TreeExpansionEvent
41 import javax.swing.event.TreeExpansionListener
42 import javax.swing.tree.TreePath
43
44 internal class BranchesTreeComponent(project: Project) : DnDAwareTree() {
45
46   var doubleClickHandler: (BranchTreeNode) -> Unit = {}
47   var searchField: SearchTextField? = null
48
49   init {
50     putClientProperty(AUTO_SELECT_ON_MOUSE_PRESSED, false)
51     setCellRenderer(BranchTreeCellRenderer(project))
52     isRootVisible = false
53     setShowsRootHandles(true)
54     isOpaque = false
55     installDoubleClickHandler()
56     SmartExpander.installOn(this)
57     initDnD()
58   }
59
60   private inner class BranchTreeCellRenderer(project: Project) : ColoredTreeCellRenderer() {
61     private val repositoryManager = GitRepositoryManager.getInstance(project)
62
63     override fun customizeCellRenderer(tree: JTree,
64                                        value: Any?,
65                                        selected: Boolean,
66                                        expanded: Boolean,
67                                        leaf: Boolean,
68                                        row: Int,
69                                        hasFocus: Boolean) {
70       if (value !is BranchTreeNode) return
71       val descriptor = value.getNodeDescriptor()
72
73       val branchInfo = descriptor.branchInfo
74       val isBranchNode = descriptor.type == NodeType.BRANCH
75       val isGroupNode = descriptor.type == NodeType.GROUP_NODE
76
77       icon = when {
78         isBranchNode && branchInfo != null && branchInfo.isCurrent && branchInfo.isFavorite -> {
79           DvcsImplIcons.CurrentBranchFavoriteLabel
80         }
81         isBranchNode && branchInfo != null && branchInfo.isCurrent -> {
82           DvcsImplIcons.CurrentBranchLabel
83         }
84         isBranchNode && branchInfo != null && branchInfo.isFavorite -> {
85           AllIcons.Nodes.Favorite
86         }
87         isBranchNode -> {
88           AllIcons.Vcs.BranchNode
89         }
90         isGroupNode -> {
91           PlatformIcons.FOLDER_ICON
92         }
93         else -> null
94       }
95
96       append(value.getTextRepresentation(), SimpleTextAttributes.REGULAR_ATTRIBUTES, true)
97
98       if (branchInfo != null && branchInfo.repositories.size < repositoryManager.repositories.size) {
99         append(" (${DvcsUtil.getShortNames(branchInfo.repositories)})", SimpleTextAttributes.GRAYED_ATTRIBUTES)
100       }
101     }
102
103     override fun calcFocusedState() = super.calcFocusedState() || searchField?.textEditor?.hasFocus() ?: false
104   }
105
106   override fun hasFocus() = super.hasFocus() || searchField?.textEditor?.hasFocus() ?: false
107
108   private fun installDoubleClickHandler() {
109     object : DoubleClickListener() {
110       override fun onDoubleClick(e: MouseEvent): Boolean {
111         val clickPath = getClosestPathForLocation(e.x, e.y) ?: return false
112         val selectionPath = selectionPath
113         if (selectionPath == null || clickPath != selectionPath) return false
114         val node = (selectionPath.lastPathComponent as? BranchTreeNode) ?: return false
115         if (isToggleEvent(this@BranchesTreeComponent, e)) return false
116
117         doubleClickHandler(node)
118         return true
119       }
120     }.installOn(this)
121   }
122
123   private fun initDnD() {
124     if (!GraphicsEnvironment.isHeadless()) {
125       transferHandler = BRANCH_TREE_TRANSFER_HANDLER
126     }
127   }
128
129   fun getSelectedBranches(): Set<BranchInfo> {
130     return getSelectedNodes()
131       .mapNotNull { it.getNodeDescriptor().branchInfo }
132       .toSet()
133   }
134
135   fun getSelectedNodes(): Sequence<BranchTreeNode> {
136     val paths = selectionPaths ?: return emptySequence()
137     return paths.asSequence()
138       .map(TreePath::getLastPathComponent)
139       .mapNotNull { it as? BranchTreeNode }
140   }
141 }
142
143 internal class FilteringBranchesTree(project: Project,
144                                      val component: BranchesTreeComponent,
145                                      private val uiController: BranchesDashboardController,
146                                      rootNode: BranchTreeNode = BranchTreeNode(BranchNodeDescriptor(NodeType.ROOT)))
147   : FilteringTree<BranchTreeNode, BranchNodeDescriptor>(project, component, rootNode) {
148
149   private val expandedPaths = SmartHashSet<TreePath>()
150
151   private val localBranchesNode = BranchTreeNode(BranchNodeDescriptor(NodeType.LOCAL_ROOT))
152   private val remoteBranchesNode = BranchTreeNode(BranchNodeDescriptor(NodeType.REMOTE_ROOT))
153   private val headBranchesNode = BranchTreeNode(BranchNodeDescriptor(NodeType.HEAD_NODE))
154   private val branchFilter: (BranchInfo) -> Boolean =
155     { branch -> !uiController.showOnlyMy || branch.isMy == ThreeState.YES }
156   private val nodeDescriptorsModel = NodeDescriptorsModel(localBranchesNode.getNodeDescriptor(),
157                                                           remoteBranchesNode.getNodeDescriptor())
158
159   private var localNodeExist = false
160   private var remoteNodeExist = false
161
162   private var useDirectoryGrouping = GitVcsSettings.getInstance(project).branchSettings.isGroupingEnabled(GroupingKey.GROUPING_BY_DIRECTORY)
163
164   fun toggleDirectoryGrouping(state: Boolean) {
165     useDirectoryGrouping = state
166     refreshTree()
167   }
168
169   init {
170     runInEdt {
171       PopupHandler.installPopupHandler(component, BranchesTreeActionGroup(project, this), "BranchesTreePopup", ActionManager.getInstance())
172       setupTreeExpansionListener()
173       project.service<BranchesTreeStateHolder>().setTree(this)
174     }
175   }
176
177   override fun createSpeedSearch(searchTextField: SearchTextField): SpeedSearchSupply =
178     object : FilteringSpeedSearch(searchTextField) {
179
180       private val customWordMatchers = hashSetOf<MinusculeMatcher>()
181
182       override fun matchingFragments(text: String): Iterable<TextRange?>? {
183         val allTextRanges = super.matchingFragments(text)
184         if (customWordMatchers.isEmpty()) return allTextRanges
185         val wordRanges = arrayListOf<TextRange>()
186         for (wordMatcher in customWordMatchers) {
187           wordMatcher.matchingFragments(text)?.let(wordRanges::addAll)
188         }
189         return when {
190           allTextRanges != null -> allTextRanges + wordRanges
191           wordRanges.isNotEmpty() -> wordRanges
192           else -> null
193         }
194       }
195
196       override fun onUpdatePattern(text: String?) {
197         customWordMatchers.clear()
198         customWordMatchers.addAll(buildCustomWordMatchers(text))
199       }
200
201       private fun buildCustomWordMatchers(text: String?): Set<MinusculeMatcher> {
202         if (text == null) return emptySet()
203
204         val wordMatchers = hashSetOf<MinusculeMatcher>()
205         for (word in StringUtil.split(text, " ")) {
206           wordMatchers.add(
207             FixingLayoutMatcher("*$word", NameUtil.MatchingCaseSensitivity.NONE, ""))
208         }
209
210         return wordMatchers
211       }
212     }
213
214   override fun installSearchField(): SearchTextField {
215     val searchField = super.installSearchField()
216     component.searchField = searchField
217     return searchField
218   }
219
220   private fun setupTreeExpansionListener() {
221     component.addTreeExpansionListener(object : TreeExpansionListener {
222       override fun treeExpanded(event: TreeExpansionEvent) {
223         expandedPaths.add(event.path)
224       }
225
226       override fun treeCollapsed(event: TreeExpansionEvent) {
227         expandedPaths.remove(event.path)
228       }
229     })
230   }
231
232   fun getSelectedBranchNames() = getSelectedBranches().map(BranchInfo::branchName)
233
234   fun getSelectedBranches() = component.getSelectedBranches()
235
236   fun getSelectedBranchFilters(): List<String> {
237     return component.getSelectedNodes()
238       .mapNotNull { with(it.getNodeDescriptor()) { if (type == NodeType.HEAD_NODE) VcsLogUtil.HEAD else branchInfo?.branchName } }
239       .toList()
240   }
241
242   private fun restorePreviouslyExpandedPaths() {
243     TreeUtil.restoreExpandedPaths(component, expandedPaths.toList())
244   }
245
246   override fun expandTreeOnSearchUpdateComplete(pattern: String?) {
247     restorePreviouslyExpandedPaths()
248   }
249
250   override fun onSpeedSearchUpdateComplete(pattern: String?) {
251     updateSpeedSearchBackground()
252   }
253
254   override fun useIdentityHashing(): Boolean = false
255
256   private fun updateSpeedSearchBackground() {
257     val speedSearch = searchModel.speedSearch as? SpeedSearch ?: return
258     val textEditor = component.searchField?.textEditor ?: return
259     if (isEmptyModel()) {
260       textEditor.isOpaque = true
261       speedSearch.noHits()
262     }
263     else {
264       textEditor.isOpaque = false
265       textEditor.background = UIUtil.getTextFieldBackground()
266     }
267   }
268
269   private fun isEmptyModel() = searchModel.isLeaf(localBranchesNode) && searchModel.isLeaf(remoteBranchesNode)
270
271   override fun getNodeClass() = BranchTreeNode::class.java
272
273   override fun createNode(nodeDescriptor: BranchNodeDescriptor) =
274     when (nodeDescriptor.type) {
275       NodeType.LOCAL_ROOT -> localBranchesNode
276       NodeType.REMOTE_ROOT -> remoteBranchesNode
277       NodeType.HEAD_NODE -> headBranchesNode
278       else -> BranchTreeNode(nodeDescriptor)
279     }
280
281   override fun getChildren(nodeDescriptor: BranchNodeDescriptor) =
282     when (nodeDescriptor.type) {
283       NodeType.ROOT -> getRootNodeDescriptors()
284       NodeType.LOCAL_ROOT -> localBranchesNode.getNodeDescriptor().getDirectChildren()
285       NodeType.REMOTE_ROOT -> remoteBranchesNode.getNodeDescriptor().getDirectChildren()
286       NodeType.GROUP_NODE -> nodeDescriptor.getDirectChildren()
287       else -> emptyList() //leaf branch node
288     }
289
290   private fun BranchNodeDescriptor.getDirectChildren() = nodeDescriptorsModel.getChildrenForParent(this)
291
292   fun update(initial: Boolean) {
293     if (rebuildTree(initial)) {
294       tree.revalidate()
295       tree.repaint()
296     }
297   }
298
299   fun rebuildTree(initial: Boolean): Boolean {
300     val rebuilded = buildTreeNodesIfNeeded()
301     val treeState = project.service<BranchesTreeStateHolder>()
302     if (!initial) {
303       treeState.createNewState()
304     }
305     searchModel.updateStructure()
306     if (initial) {
307       treeState.applyStateToTreeOrExpandAll()
308     }
309     else {
310       treeState.applyStateToTree()
311     }
312
313     return rebuilded
314   }
315
316   fun refreshTree() {
317     val treeState = project.service<BranchesTreeStateHolder>()
318     treeState.createNewState()
319     tree.selectionModel.clearSelection()
320     refreshNodeDescriptorsModel()
321     searchModel.updateStructure()
322     treeState.applyStateToTree()
323   }
324
325   private fun buildTreeNodesIfNeeded(): Boolean {
326     with(uiController) {
327       val changed = checkForBranchesUpdate()
328       if (!changed) return false
329
330       refreshNodeDescriptorsModel()
331
332       return changed
333     }
334   }
335
336   private fun refreshNodeDescriptorsModel() {
337     with(uiController) {
338       nodeDescriptorsModel.clear()
339
340       localNodeExist = localBranches.isNotEmpty()
341       remoteNodeExist = remoteBranches.isNotEmpty()
342
343       nodeDescriptorsModel.populateFrom((localBranches.asSequence() + remoteBranches.asSequence()).filter(branchFilter), useDirectoryGrouping)
344     }
345   }
346
347   override fun getText(nodeDescriptor: BranchNodeDescriptor?) = nodeDescriptor?.branchInfo?.branchName ?: nodeDescriptor?.displayName
348
349   private fun getRootNodeDescriptors() =
350     mutableListOf<BranchNodeDescriptor>().apply {
351       add(headBranchesNode.getNodeDescriptor())
352       if (localNodeExist) add(localBranchesNode.getNodeDescriptor())
353       if (remoteNodeExist) add(remoteBranchesNode.getNodeDescriptor())
354     }
355 }
356
357 private val BRANCH_TREE_TRANSFER_HANDLER = object : TransferHandler() {
358   override fun createTransferable(tree: JComponent): Transferable? {
359     if (tree is BranchesTreeComponent) {
360       val branches = tree.getSelectedBranches()
361       if (branches.isEmpty()) return null
362
363       return object : TransferableList<BranchInfo>(branches.toList()) {
364         override fun toString(branch: BranchInfo) = branch.toString()
365       }
366     }
367     return null
368   }
369
370   override fun getSourceActions(c: JComponent) = COPY_OR_MOVE
371 }
372
373 @State(name = "BranchesTreeState", storages = [Storage(StoragePathMacros.PRODUCT_WORKSPACE_FILE)], reportStatistic = false)
374 internal class BranchesTreeStateHolder : PersistentStateComponent<TreeState> {
375   private lateinit var branchesTree: FilteringBranchesTree
376   private lateinit var treeState: TreeState
377
378   override fun getState(): TreeState? {
379     createNewState()
380     if (::treeState.isInitialized) {
381       return treeState
382     }
383     return null
384   }
385
386   override fun loadState(state: TreeState) {
387     treeState = state
388   }
389
390   fun createNewState() {
391     if (::branchesTree.isInitialized) {
392       treeState = TreeState.createOn(branchesTree.tree, branchesTree.root)
393     }
394   }
395
396   fun applyStateToTree(ifNoStatePresent: () -> Unit = {}) {
397     if (!::branchesTree.isInitialized) return
398
399     if (::treeState.isInitialized) {
400       treeState.applyTo(branchesTree.tree)
401     }
402     else {
403       ifNoStatePresent()
404     }
405   }
406
407   fun applyStateToTreeOrExpandAll() = applyStateToTree { TreeUtil.expandAll(branchesTree.tree) }
408
409   fun setTree(tree: FilteringBranchesTree) {
410     branchesTree = tree
411   }
412 }