ba9230a42d5f20c80b160d1a18b215c114971a64
[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   fun getSelectedRemotes(): Set<String> {
143     val paths = selectionPaths ?: return emptySet()
144     return paths.asSequence()
145       .map(TreePath::getLastPathComponent)
146       .mapNotNull { it as? BranchTreeNode }
147       .filter { it.getNodeDescriptor().type == NodeType.GROUP_NODE && it.getNodeDescriptor().parent?.type == NodeType.REMOTE_ROOT }
148       .mapNotNull { it.getNodeDescriptor().displayName }
149       .toSet()
150   }
151 }
152
153 internal class FilteringBranchesTree(project: Project,
154                                      val component: BranchesTreeComponent,
155                                      private val uiController: BranchesDashboardController,
156                                      rootNode: BranchTreeNode = BranchTreeNode(BranchNodeDescriptor(NodeType.ROOT)))
157   : FilteringTree<BranchTreeNode, BranchNodeDescriptor>(project, component, rootNode) {
158
159   private val expandedPaths = SmartHashSet<TreePath>()
160
161   private val localBranchesNode = BranchTreeNode(BranchNodeDescriptor(NodeType.LOCAL_ROOT))
162   private val remoteBranchesNode = BranchTreeNode(BranchNodeDescriptor(NodeType.REMOTE_ROOT))
163   private val headBranchesNode = BranchTreeNode(BranchNodeDescriptor(NodeType.HEAD_NODE))
164   private val branchFilter: (BranchInfo) -> Boolean =
165     { branch -> !uiController.showOnlyMy || branch.isMy == ThreeState.YES }
166   private val nodeDescriptorsModel = NodeDescriptorsModel(localBranchesNode.getNodeDescriptor(),
167                                                           remoteBranchesNode.getNodeDescriptor())
168
169   private var localNodeExist = false
170   private var remoteNodeExist = false
171
172   private var useDirectoryGrouping = GitVcsSettings.getInstance(project).branchSettings.isGroupingEnabled(GroupingKey.GROUPING_BY_DIRECTORY)
173
174   fun toggleDirectoryGrouping(state: Boolean) {
175     useDirectoryGrouping = state
176     refreshTree()
177   }
178
179   init {
180     runInEdt {
181       PopupHandler.installPopupHandler(component, BranchesTreeActionGroup(project, this), "BranchesTreePopup", ActionManager.getInstance())
182       setupTreeExpansionListener()
183       project.service<BranchesTreeStateHolder>().setTree(this)
184     }
185   }
186
187   override fun createSpeedSearch(searchTextField: SearchTextField): SpeedSearchSupply =
188     object : FilteringSpeedSearch(searchTextField) {
189
190       private val customWordMatchers = hashSetOf<MinusculeMatcher>()
191
192       override fun matchingFragments(text: String): Iterable<TextRange?>? {
193         val allTextRanges = super.matchingFragments(text)
194         if (customWordMatchers.isEmpty()) return allTextRanges
195         val wordRanges = arrayListOf<TextRange>()
196         for (wordMatcher in customWordMatchers) {
197           wordMatcher.matchingFragments(text)?.let(wordRanges::addAll)
198         }
199         return when {
200           allTextRanges != null -> allTextRanges + wordRanges
201           wordRanges.isNotEmpty() -> wordRanges
202           else -> null
203         }
204       }
205
206       override fun onUpdatePattern(text: String?) {
207         customWordMatchers.clear()
208         customWordMatchers.addAll(buildCustomWordMatchers(text))
209       }
210
211       private fun buildCustomWordMatchers(text: String?): Set<MinusculeMatcher> {
212         if (text == null) return emptySet()
213
214         val wordMatchers = hashSetOf<MinusculeMatcher>()
215         for (word in StringUtil.split(text, " ")) {
216           wordMatchers.add(
217             FixingLayoutMatcher("*$word", NameUtil.MatchingCaseSensitivity.NONE, ""))
218         }
219
220         return wordMatchers
221       }
222     }
223
224   override fun installSearchField(): SearchTextField {
225     val searchField = super.installSearchField()
226     component.searchField = searchField
227     return searchField
228   }
229
230   private fun setupTreeExpansionListener() {
231     component.addTreeExpansionListener(object : TreeExpansionListener {
232       override fun treeExpanded(event: TreeExpansionEvent) {
233         expandedPaths.add(event.path)
234       }
235
236       override fun treeCollapsed(event: TreeExpansionEvent) {
237         expandedPaths.remove(event.path)
238       }
239     })
240   }
241
242   fun getSelectedBranchNames() = getSelectedBranches().map(BranchInfo::branchName)
243
244   fun getSelectedBranches() = component.getSelectedBranches()
245
246   fun getSelectedBranchFilters(): List<String> {
247     return component.getSelectedNodes()
248       .mapNotNull { with(it.getNodeDescriptor()) { if (type == NodeType.HEAD_NODE) VcsLogUtil.HEAD else branchInfo?.branchName } }
249       .toList()
250   }
251
252   fun getSelectedRemotes() = component.getSelectedRemotes()
253
254   private fun restorePreviouslyExpandedPaths() {
255     TreeUtil.restoreExpandedPaths(component, expandedPaths.toList())
256   }
257
258   override fun expandTreeOnSearchUpdateComplete(pattern: String?) {
259     restorePreviouslyExpandedPaths()
260   }
261
262   override fun onSpeedSearchUpdateComplete(pattern: String?) {
263     updateSpeedSearchBackground()
264   }
265
266   override fun useIdentityHashing(): Boolean = false
267
268   private fun updateSpeedSearchBackground() {
269     val speedSearch = searchModel.speedSearch as? SpeedSearch ?: return
270     val textEditor = component.searchField?.textEditor ?: return
271     if (isEmptyModel()) {
272       textEditor.isOpaque = true
273       speedSearch.noHits()
274     }
275     else {
276       textEditor.isOpaque = false
277       textEditor.background = UIUtil.getTextFieldBackground()
278     }
279   }
280
281   private fun isEmptyModel() = searchModel.isLeaf(localBranchesNode) && searchModel.isLeaf(remoteBranchesNode)
282
283   override fun getNodeClass() = BranchTreeNode::class.java
284
285   override fun createNode(nodeDescriptor: BranchNodeDescriptor) =
286     when (nodeDescriptor.type) {
287       NodeType.LOCAL_ROOT -> localBranchesNode
288       NodeType.REMOTE_ROOT -> remoteBranchesNode
289       NodeType.HEAD_NODE -> headBranchesNode
290       else -> BranchTreeNode(nodeDescriptor)
291     }
292
293   override fun getChildren(nodeDescriptor: BranchNodeDescriptor) =
294     when (nodeDescriptor.type) {
295       NodeType.ROOT -> getRootNodeDescriptors()
296       NodeType.LOCAL_ROOT -> localBranchesNode.getNodeDescriptor().getDirectChildren()
297       NodeType.REMOTE_ROOT -> remoteBranchesNode.getNodeDescriptor().getDirectChildren()
298       NodeType.GROUP_NODE -> nodeDescriptor.getDirectChildren()
299       else -> emptyList() //leaf branch node
300     }
301
302   private fun BranchNodeDescriptor.getDirectChildren() = nodeDescriptorsModel.getChildrenForParent(this)
303
304   fun update(initial: Boolean) {
305     if (rebuildTree(initial)) {
306       tree.revalidate()
307       tree.repaint()
308     }
309   }
310
311   fun rebuildTree(initial: Boolean): Boolean {
312     val rebuilded = buildTreeNodesIfNeeded()
313     val treeState = project.service<BranchesTreeStateHolder>()
314     if (!initial) {
315       treeState.createNewState()
316     }
317     searchModel.updateStructure()
318     if (initial) {
319       treeState.applyStateToTreeOrExpandAll()
320     }
321     else {
322       treeState.applyStateToTree()
323     }
324
325     return rebuilded
326   }
327
328   fun refreshTree() {
329     val treeState = project.service<BranchesTreeStateHolder>()
330     treeState.createNewState()
331     tree.selectionModel.clearSelection()
332     refreshNodeDescriptorsModel()
333     searchModel.updateStructure()
334     treeState.applyStateToTree()
335   }
336
337   private fun buildTreeNodesIfNeeded(): Boolean {
338     with(uiController) {
339       val changed = checkForBranchesUpdate()
340       if (!changed) return false
341
342       refreshNodeDescriptorsModel()
343
344       return changed
345     }
346   }
347
348   private fun refreshNodeDescriptorsModel() {
349     with(uiController) {
350       nodeDescriptorsModel.clear()
351
352       localNodeExist = localBranches.isNotEmpty()
353       remoteNodeExist = remoteBranches.isNotEmpty()
354
355       nodeDescriptorsModel.populateFrom((localBranches.asSequence() + remoteBranches.asSequence()).filter(branchFilter), useDirectoryGrouping)
356     }
357   }
358
359   override fun getText(nodeDescriptor: BranchNodeDescriptor?) = nodeDescriptor?.branchInfo?.branchName ?: nodeDescriptor?.displayName
360
361   private fun getRootNodeDescriptors() =
362     mutableListOf<BranchNodeDescriptor>().apply {
363       add(headBranchesNode.getNodeDescriptor())
364       if (localNodeExist) add(localBranchesNode.getNodeDescriptor())
365       if (remoteNodeExist) add(remoteBranchesNode.getNodeDescriptor())
366     }
367 }
368
369 private val BRANCH_TREE_TRANSFER_HANDLER = object : TransferHandler() {
370   override fun createTransferable(tree: JComponent): Transferable? {
371     if (tree is BranchesTreeComponent) {
372       val branches = tree.getSelectedBranches()
373       if (branches.isEmpty()) return null
374
375       return object : TransferableList<BranchInfo>(branches.toList()) {
376         override fun toString(branch: BranchInfo) = branch.toString()
377       }
378     }
379     return null
380   }
381
382   override fun getSourceActions(c: JComponent) = COPY_OR_MOVE
383 }
384
385 @State(name = "BranchesTreeState", storages = [Storage(StoragePathMacros.PRODUCT_WORKSPACE_FILE)], reportStatistic = false)
386 internal class BranchesTreeStateHolder : PersistentStateComponent<TreeState> {
387   private lateinit var branchesTree: FilteringBranchesTree
388   private lateinit var treeState: TreeState
389
390   override fun getState(): TreeState? {
391     createNewState()
392     if (::treeState.isInitialized) {
393       return treeState
394     }
395     return null
396   }
397
398   override fun loadState(state: TreeState) {
399     treeState = state
400   }
401
402   fun createNewState() {
403     if (::branchesTree.isInitialized) {
404       treeState = TreeState.createOn(branchesTree.tree, branchesTree.root)
405     }
406   }
407
408   fun applyStateToTree(ifNoStatePresent: () -> Unit = {}) {
409     if (!::branchesTree.isInitialized) return
410
411     if (::treeState.isInitialized) {
412       treeState.applyTo(branchesTree.tree)
413     }
414     else {
415       ifNoStatePresent()
416     }
417   }
418
419   fun applyStateToTreeOrExpandAll() = applyStateToTree { TreeUtil.expandAll(branchesTree.tree) }
420
421   fun setTree(tree: FilteringBranchesTree) {
422     branchesTree = tree
423   }
424 }