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
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
44 internal class BranchesTreeComponent(project: Project) : DnDAwareTree() {
46 var doubleClickHandler: (BranchTreeNode) -> Unit = {}
47 var searchField: SearchTextField? = null
50 putClientProperty(AUTO_SELECT_ON_MOUSE_PRESSED, false)
51 setCellRenderer(BranchTreeCellRenderer(project))
53 setShowsRootHandles(true)
55 installDoubleClickHandler()
56 SmartExpander.installOn(this)
60 private inner class BranchTreeCellRenderer(project: Project) : ColoredTreeCellRenderer() {
61 private val repositoryManager = GitRepositoryManager.getInstance(project)
63 override fun customizeCellRenderer(tree: JTree,
70 if (value !is BranchTreeNode) return
71 val descriptor = value.getNodeDescriptor()
73 val branchInfo = descriptor.branchInfo
74 val isBranchNode = descriptor.type == NodeType.BRANCH
75 val isGroupNode = descriptor.type == NodeType.GROUP_NODE
78 isBranchNode && branchInfo != null && branchInfo.isCurrent && branchInfo.isFavorite -> {
79 DvcsImplIcons.CurrentBranchFavoriteLabel
81 isBranchNode && branchInfo != null && branchInfo.isCurrent -> {
82 DvcsImplIcons.CurrentBranchLabel
84 isBranchNode && branchInfo != null && branchInfo.isFavorite -> {
85 AllIcons.Nodes.Favorite
88 AllIcons.Vcs.BranchNode
91 PlatformIcons.FOLDER_ICON
96 append(value.getTextRepresentation(), SimpleTextAttributes.REGULAR_ATTRIBUTES, true)
98 if (branchInfo != null && branchInfo.repositories.size < repositoryManager.repositories.size) {
99 append(" (${DvcsUtil.getShortNames(branchInfo.repositories)})", SimpleTextAttributes.GRAYED_ATTRIBUTES)
103 override fun calcFocusedState() = super.calcFocusedState() || searchField?.textEditor?.hasFocus() ?: false
106 override fun hasFocus() = super.hasFocus() || searchField?.textEditor?.hasFocus() ?: false
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
117 doubleClickHandler(node)
123 private fun initDnD() {
124 if (!GraphicsEnvironment.isHeadless()) {
125 transferHandler = BRANCH_TREE_TRANSFER_HANDLER
129 fun getSelectedBranches(): Set<BranchInfo> {
130 return getSelectedNodes()
131 .mapNotNull { it.getNodeDescriptor().branchInfo }
135 fun getSelectedNodes(): Sequence<BranchTreeNode> {
136 val paths = selectionPaths ?: return emptySequence()
137 return paths.asSequence()
138 .map(TreePath::getLastPathComponent)
139 .mapNotNull { it as? BranchTreeNode }
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 }
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) {
159 private val expandedPaths = SmartHashSet<TreePath>()
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())
169 private var localNodeExist = false
170 private var remoteNodeExist = false
172 private var useDirectoryGrouping = GitVcsSettings.getInstance(project).branchSettings.isGroupingEnabled(GroupingKey.GROUPING_BY_DIRECTORY)
174 fun toggleDirectoryGrouping(state: Boolean) {
175 useDirectoryGrouping = state
181 PopupHandler.installPopupHandler(component, BranchesTreeActionGroup(project, this), "BranchesTreePopup", ActionManager.getInstance())
182 setupTreeExpansionListener()
183 project.service<BranchesTreeStateHolder>().setTree(this)
187 override fun createSpeedSearch(searchTextField: SearchTextField): SpeedSearchSupply =
188 object : FilteringSpeedSearch(searchTextField) {
190 private val customWordMatchers = hashSetOf<MinusculeMatcher>()
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)
200 allTextRanges != null -> allTextRanges + wordRanges
201 wordRanges.isNotEmpty() -> wordRanges
206 override fun onUpdatePattern(text: String?) {
207 customWordMatchers.clear()
208 customWordMatchers.addAll(buildCustomWordMatchers(text))
211 private fun buildCustomWordMatchers(text: String?): Set<MinusculeMatcher> {
212 if (text == null) return emptySet()
214 val wordMatchers = hashSetOf<MinusculeMatcher>()
215 for (word in StringUtil.split(text, " ")) {
217 FixingLayoutMatcher("*$word", NameUtil.MatchingCaseSensitivity.NONE, ""))
224 override fun installSearchField(): SearchTextField {
225 val searchField = super.installSearchField()
226 component.searchField = searchField
230 private fun setupTreeExpansionListener() {
231 component.addTreeExpansionListener(object : TreeExpansionListener {
232 override fun treeExpanded(event: TreeExpansionEvent) {
233 expandedPaths.add(event.path)
236 override fun treeCollapsed(event: TreeExpansionEvent) {
237 expandedPaths.remove(event.path)
242 fun getSelectedBranchNames() = getSelectedBranches().map(BranchInfo::branchName)
244 fun getSelectedBranches() = component.getSelectedBranches()
246 fun getSelectedBranchFilters(): List<String> {
247 return component.getSelectedNodes()
248 .mapNotNull { with(it.getNodeDescriptor()) { if (type == NodeType.HEAD_NODE) VcsLogUtil.HEAD else branchInfo?.branchName } }
252 fun getSelectedRemotes() = component.getSelectedRemotes()
254 private fun restorePreviouslyExpandedPaths() {
255 TreeUtil.restoreExpandedPaths(component, expandedPaths.toList())
258 override fun expandTreeOnSearchUpdateComplete(pattern: String?) {
259 restorePreviouslyExpandedPaths()
262 override fun onSpeedSearchUpdateComplete(pattern: String?) {
263 updateSpeedSearchBackground()
266 override fun useIdentityHashing(): Boolean = false
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
276 textEditor.isOpaque = false
277 textEditor.background = UIUtil.getTextFieldBackground()
281 private fun isEmptyModel() = searchModel.isLeaf(localBranchesNode) && searchModel.isLeaf(remoteBranchesNode)
283 override fun getNodeClass() = BranchTreeNode::class.java
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)
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
302 private fun BranchNodeDescriptor.getDirectChildren() = nodeDescriptorsModel.getChildrenForParent(this)
304 fun update(initial: Boolean) {
305 if (rebuildTree(initial)) {
311 fun rebuildTree(initial: Boolean): Boolean {
312 val rebuilded = buildTreeNodesIfNeeded()
313 val treeState = project.service<BranchesTreeStateHolder>()
315 treeState.createNewState()
317 searchModel.updateStructure()
319 treeState.applyStateToTreeOrExpandAll()
322 treeState.applyStateToTree()
329 val treeState = project.service<BranchesTreeStateHolder>()
330 treeState.createNewState()
331 tree.selectionModel.clearSelection()
332 refreshNodeDescriptorsModel()
333 searchModel.updateStructure()
334 treeState.applyStateToTree()
337 private fun buildTreeNodesIfNeeded(): Boolean {
339 val changed = checkForBranchesUpdate()
340 if (!changed) return false
342 refreshNodeDescriptorsModel()
348 private fun refreshNodeDescriptorsModel() {
350 nodeDescriptorsModel.clear()
352 localNodeExist = localBranches.isNotEmpty()
353 remoteNodeExist = remoteBranches.isNotEmpty()
355 nodeDescriptorsModel.populateFrom((localBranches.asSequence() + remoteBranches.asSequence()).filter(branchFilter), useDirectoryGrouping)
359 override fun getText(nodeDescriptor: BranchNodeDescriptor?) = nodeDescriptor?.branchInfo?.branchName ?: nodeDescriptor?.displayName
361 private fun getRootNodeDescriptors() =
362 mutableListOf<BranchNodeDescriptor>().apply {
363 add(headBranchesNode.getNodeDescriptor())
364 if (localNodeExist) add(localBranchesNode.getNodeDescriptor())
365 if (remoteNodeExist) add(remoteBranchesNode.getNodeDescriptor())
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
375 return object : TransferableList<BranchInfo>(branches.toList()) {
376 override fun toString(branch: BranchInfo) = branch.toString()
382 override fun getSourceActions(c: JComponent) = COPY_OR_MOVE
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
390 override fun getState(): TreeState? {
392 if (::treeState.isInitialized) {
398 override fun loadState(state: TreeState) {
402 fun createNewState() {
403 if (::branchesTree.isInitialized) {
404 treeState = TreeState.createOn(branchesTree.tree, branchesTree.root)
408 fun applyStateToTree(ifNoStatePresent: () -> Unit = {}) {
409 if (!::branchesTree.isInitialized) return
411 if (::treeState.isInitialized) {
412 treeState.applyTo(branchesTree.tree)
419 fun applyStateToTreeOrExpandAll() = applyStateToTree { TreeUtil.expandAll(branchesTree.tree) }
421 fun setTree(tree: FilteringBranchesTree) {