git-branches-dashboard: add ability to open manage remotes dialog from Remote node...
[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   fun getSelectedBranchNodes() = component.getSelectedNodes().map(BranchTreeNode::getNodeDescriptor).toSet()
255
256   private fun restorePreviouslyExpandedPaths() {
257     TreeUtil.restoreExpandedPaths(component, expandedPaths.toList())
258   }
259
260   override fun expandTreeOnSearchUpdateComplete(pattern: String?) {
261     restorePreviouslyExpandedPaths()
262   }
263
264   override fun onSpeedSearchUpdateComplete(pattern: String?) {
265     updateSpeedSearchBackground()
266   }
267
268   override fun useIdentityHashing(): Boolean = false
269
270   private fun updateSpeedSearchBackground() {
271     val speedSearch = searchModel.speedSearch as? SpeedSearch ?: return
272     val textEditor = component.searchField?.textEditor ?: return
273     if (isEmptyModel()) {
274       textEditor.isOpaque = true
275       speedSearch.noHits()
276     }
277     else {
278       textEditor.isOpaque = false
279       textEditor.background = UIUtil.getTextFieldBackground()
280     }
281   }
282
283   private fun isEmptyModel() = searchModel.isLeaf(localBranchesNode) && searchModel.isLeaf(remoteBranchesNode)
284
285   override fun getNodeClass() = BranchTreeNode::class.java
286
287   override fun createNode(nodeDescriptor: BranchNodeDescriptor) =
288     when (nodeDescriptor.type) {
289       NodeType.LOCAL_ROOT -> localBranchesNode
290       NodeType.REMOTE_ROOT -> remoteBranchesNode
291       NodeType.HEAD_NODE -> headBranchesNode
292       else -> BranchTreeNode(nodeDescriptor)
293     }
294
295   override fun getChildren(nodeDescriptor: BranchNodeDescriptor) =
296     when (nodeDescriptor.type) {
297       NodeType.ROOT -> getRootNodeDescriptors()
298       NodeType.LOCAL_ROOT -> localBranchesNode.getNodeDescriptor().getDirectChildren()
299       NodeType.REMOTE_ROOT -> remoteBranchesNode.getNodeDescriptor().getDirectChildren()
300       NodeType.GROUP_NODE -> nodeDescriptor.getDirectChildren()
301       else -> emptyList() //leaf branch node
302     }
303
304   private fun BranchNodeDescriptor.getDirectChildren() = nodeDescriptorsModel.getChildrenForParent(this)
305
306   fun update(initial: Boolean) {
307     if (rebuildTree(initial)) {
308       tree.revalidate()
309       tree.repaint()
310     }
311   }
312
313   fun rebuildTree(initial: Boolean): Boolean {
314     val rebuilded = buildTreeNodesIfNeeded()
315     val treeState = project.service<BranchesTreeStateHolder>()
316     if (!initial) {
317       treeState.createNewState()
318     }
319     searchModel.updateStructure()
320     if (initial) {
321       treeState.applyStateToTreeOrExpandAll()
322     }
323     else {
324       treeState.applyStateToTree()
325     }
326
327     return rebuilded
328   }
329
330   fun refreshTree() {
331     val treeState = project.service<BranchesTreeStateHolder>()
332     treeState.createNewState()
333     tree.selectionModel.clearSelection()
334     refreshNodeDescriptorsModel()
335     searchModel.updateStructure()
336     treeState.applyStateToTree()
337   }
338
339   private fun buildTreeNodesIfNeeded(): Boolean {
340     with(uiController) {
341       val changed = checkForBranchesUpdate()
342       if (!changed) return false
343
344       refreshNodeDescriptorsModel()
345
346       return changed
347     }
348   }
349
350   private fun refreshNodeDescriptorsModel() {
351     with(uiController) {
352       nodeDescriptorsModel.clear()
353
354       localNodeExist = localBranches.isNotEmpty()
355       remoteNodeExist = remoteBranches.isNotEmpty()
356
357       nodeDescriptorsModel.populateFrom((localBranches.asSequence() + remoteBranches.asSequence()).filter(branchFilter), useDirectoryGrouping)
358     }
359   }
360
361   override fun getText(nodeDescriptor: BranchNodeDescriptor?) = nodeDescriptor?.branchInfo?.branchName ?: nodeDescriptor?.displayName
362
363   private fun getRootNodeDescriptors() =
364     mutableListOf<BranchNodeDescriptor>().apply {
365       add(headBranchesNode.getNodeDescriptor())
366       if (localNodeExist) add(localBranchesNode.getNodeDescriptor())
367       if (remoteNodeExist) add(remoteBranchesNode.getNodeDescriptor())
368     }
369 }
370
371 private val BRANCH_TREE_TRANSFER_HANDLER = object : TransferHandler() {
372   override fun createTransferable(tree: JComponent): Transferable? {
373     if (tree is BranchesTreeComponent) {
374       val branches = tree.getSelectedBranches()
375       if (branches.isEmpty()) return null
376
377       return object : TransferableList<BranchInfo>(branches.toList()) {
378         override fun toString(branch: BranchInfo) = branch.toString()
379       }
380     }
381     return null
382   }
383
384   override fun getSourceActions(c: JComponent) = COPY_OR_MOVE
385 }
386
387 @State(name = "BranchesTreeState", storages = [Storage(StoragePathMacros.PRODUCT_WORKSPACE_FILE)], reportStatistic = false)
388 internal class BranchesTreeStateHolder : PersistentStateComponent<TreeState> {
389   private lateinit var branchesTree: FilteringBranchesTree
390   private lateinit var treeState: TreeState
391
392   override fun getState(): TreeState? {
393     createNewState()
394     if (::treeState.isInitialized) {
395       return treeState
396     }
397     return null
398   }
399
400   override fun loadState(state: TreeState) {
401     treeState = state
402   }
403
404   fun createNewState() {
405     if (::branchesTree.isInitialized) {
406       treeState = TreeState.createOn(branchesTree.tree, branchesTree.root)
407     }
408   }
409
410   fun applyStateToTree(ifNoStatePresent: () -> Unit = {}) {
411     if (!::branchesTree.isInitialized) return
412
413     if (::treeState.isInitialized) {
414       treeState.applyTo(branchesTree.tree)
415     }
416     else {
417       ifNoStatePresent()
418     }
419   }
420
421   fun applyStateToTreeOrExpandAll() = applyStateToTree { TreeUtil.expandAll(branchesTree.tree) }
422
423   fun setTree(tree: FilteringBranchesTree) {
424     branchesTree = tree
425   }
426 }