git-branches-dashboard: add edit/remove actions for Git remotes
[idea/community.git] / plugins / git4idea / src / git4idea / ui / branch / dashboard / BranchesDashboardUi.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.icons.AllIcons
5 import com.intellij.ide.CommonActionsManager
6 import com.intellij.ide.DataManager
7 import com.intellij.ide.DefaultTreeExpander
8 import com.intellij.openapi.Disposable
9 import com.intellij.openapi.actionSystem.*
10 import com.intellij.openapi.keymap.KeymapUtil
11 import com.intellij.openapi.progress.util.ProgressWindow
12 import com.intellij.openapi.project.DumbAwareAction
13 import com.intellij.openapi.project.Project
14 import com.intellij.openapi.util.Disposer
15 import com.intellij.openapi.vcs.changes.ui.ChangesBrowserBase
16 import com.intellij.openapi.wm.IdeFocusManager
17 import com.intellij.ui.IdeBorderFactory.createBorder
18 import com.intellij.ui.JBColor
19 import com.intellij.ui.OnePixelSplitter
20 import com.intellij.ui.ScrollPaneFactory
21 import com.intellij.ui.SideBorder
22 import com.intellij.ui.components.panels.NonOpaquePanel
23 import com.intellij.util.ui.JBUI
24 import com.intellij.util.ui.JBUI.Panels.simplePanel
25 import com.intellij.util.ui.StatusText.getDefaultEmptyText
26 import com.intellij.util.ui.UIUtil
27 import com.intellij.util.ui.components.BorderLayoutPanel
28 import com.intellij.util.ui.table.ComponentsListFocusTraversalPolicy
29 import com.intellij.vcs.log.VcsLogBranchLikeFilter
30 import com.intellij.vcs.log.VcsLogFilterCollection
31 import com.intellij.vcs.log.data.VcsLogData
32 import com.intellij.vcs.log.impl.*
33 import com.intellij.vcs.log.impl.VcsLogManager.BaseVcsLogUiFactory
34 import com.intellij.vcs.log.impl.VcsLogProjectTabsProperties.MAIN_LOG_ID
35 import com.intellij.vcs.log.ui.VcsLogColorManager
36 import com.intellij.vcs.log.ui.VcsLogInternalDataKeys
37 import com.intellij.vcs.log.ui.VcsLogUiImpl
38 import com.intellij.vcs.log.ui.filter.VcsLogFilterUiEx
39 import com.intellij.vcs.log.ui.frame.*
40 import com.intellij.vcs.log.util.VcsLogUiUtil.isDiffPreviewInEditor
41 import com.intellij.vcs.log.visible.VisiblePackRefresher
42 import com.intellij.vcs.log.visible.VisiblePackRefresherImpl
43 import com.intellij.vcs.log.visible.filters.VcsLogFilterObject
44 import com.intellij.vcs.log.visible.filters.with
45 import com.intellij.vcs.log.visible.filters.without
46 import git4idea.i18n.GitBundle.message
47 import git4idea.i18n.GitBundleExtensions.messagePointer
48 import git4idea.ui.branch.dashboard.BranchesDashboardActions.DeleteBranchAction
49 import git4idea.ui.branch.dashboard.BranchesDashboardActions.FetchAction
50 import git4idea.ui.branch.dashboard.BranchesDashboardActions.GroupBranchByDirectoryAction
51 import git4idea.ui.branch.dashboard.BranchesDashboardActions.NewBranchAction
52 import git4idea.ui.branch.dashboard.BranchesDashboardActions.ShowBranchDiffAction
53 import git4idea.ui.branch.dashboard.BranchesDashboardActions.ShowMyBranchesAction
54 import git4idea.ui.branch.dashboard.BranchesDashboardActions.ToggleFavoriteAction
55 import git4idea.ui.branch.dashboard.BranchesDashboardActions.UpdateSelectedBranchAction
56 import org.jetbrains.annotations.CalledInAwt
57 import java.awt.Component
58 import javax.swing.JComponent
59 import javax.swing.event.TreeSelectionListener
60
61 internal class BranchesDashboardUi(project: Project, private val logUi: BranchesVcsLogUi) : Disposable {
62   private val uiController = BranchesDashboardController(project, this)
63
64   private val tree = FilteringBranchesTree(project, BranchesTreeComponent(project), uiController)
65   private val branchViewSplitter = BranchViewSplitter()
66   private val branchesTreePanel = BranchesTreePanel().withBorder(createBorder(JBColor.border(), SideBorder.LEFT))
67   private val branchesScrollPane = ScrollPaneFactory.createScrollPane(tree.component, true)
68   private val branchesProgressStripe = ProgressStripe(branchesScrollPane, this, ProgressWindow.DEFAULT_PROGRESS_DIALOG_POSTPONE_TIME_MILLIS)
69   private val branchesTreeWithLogPanel = simplePanel()
70   private val mainPanel = simplePanel().apply { DataManager.registerDataProvider(this, uiController) }
71   private val branchesSearchFieldPanel = simplePanel()
72   private val branchesSearchField =
73     NonOpaquePanel(tree.installSearchField().apply { textEditor.border = JBUI.Borders.emptyLeft(5) }).apply(UIUtil::setNotOpaqueRecursively)
74
75   private lateinit var branchesPanelExpandableController: ExpandablePanelController
76
77   private val treeSelectionListener = TreeSelectionListener {
78     if (!branchesPanelExpandableController.isExpanded()) return@TreeSelectionListener
79
80     val ui = logUi
81
82     val properties = ui.properties
83     val changeLogFilterAllowed = properties[CHANGE_LOG_FILTER_ON_BRANCH_SELECTION_PROPERTY]
84     if (!changeLogFilterAllowed) return@TreeSelectionListener
85
86     updateLogBranchFilter()
87   }
88
89   internal fun updateLogBranchFilter() {
90     val ui = logUi
91     val selectedFilters = tree.getSelectedBranchFilters()
92     val oldFilters = ui.filterUi.filters
93     val newFilters = if (selectedFilters.isNotEmpty()) {
94       oldFilters.without(VcsLogBranchLikeFilter::class.java).with(VcsLogFilterObject.fromBranches(selectedFilters))
95     } else {
96       oldFilters.without(VcsLogBranchLikeFilter::class.java)
97     }
98     ui.filterUi.filters = newFilters
99   }
100
101   private val BRANCHES_UI_FOCUS_TRAVERSAL_POLICY = object : ComponentsListFocusTraversalPolicy() {
102     override fun getOrderedComponents(): List<Component> = listOf(tree.component, logUi.table,
103                                                                   logUi.changesBrowser.preferredFocusedComponent,
104                                                                   logUi.filterUi.textFilterComponent.textEditor)
105   }
106
107   private val showBranches get() = logUi.properties.get(SHOW_GIT_BRANCHES_LOG_PROPERTY)
108
109   init {
110     initMainUi()
111     installLogUi()
112     toggleBranchesPanelVisibility()
113   }
114
115   @CalledInAwt
116   private fun installLogUi() {
117     uiController.registerDataPackListener(logUi.logData)
118     uiController.registerLogUiPropertiesListener(logUi.properties)
119     branchesSearchField.setVerticalSizeReferent(logUi.toolbar)
120     branchViewSplitter.secondComponent = logUi.mainLogComponent
121     val isDiffPreviewInEditor = isDiffPreviewInEditor()
122     val diffPreview = logUi.createDiffPreview(isDiffPreviewInEditor)
123     if (isDiffPreviewInEditor) {
124       mainPanel.add(branchesTreeWithLogPanel)
125     }
126     else {
127       mainPanel.add(DiffPreviewSplitter(diffPreview, logUi.properties, branchesTreeWithLogPanel).mainComponent)
128     }
129     tree.component.addTreeSelectionListener(treeSelectionListener)
130   }
131
132   @CalledInAwt
133   private fun disposeBranchesUi() {
134     branchViewSplitter.secondComponent.removeAll()
135     uiController.removeDataPackListener(logUi.logData)
136     uiController.removeLogUiPropertiesListener(logUi.properties)
137     tree.component.removeTreeSelectionListener(treeSelectionListener)
138   }
139
140   private fun initMainUi() {
141     val diffAction = ShowBranchDiffAction()
142     diffAction.registerCustomShortcutSet(KeymapUtil.getActiveKeymapShortcuts("Diff.ShowDiff"), branchesTreeWithLogPanel)
143
144     val deleteAction = DeleteBranchAction()
145     val shortcuts = KeymapUtil.getActiveKeymapShortcuts("SafeDelete").shortcuts + KeymapUtil.getActiveKeymapShortcuts(
146       "EditorDeleteToLineStart").shortcuts
147     deleteAction.registerCustomShortcutSet(CustomShortcutSet(*shortcuts), branchesTreeWithLogPanel)
148
149     createFocusFilterFieldAction(branchesSearchField)
150
151     val groupByDirectoryAction = GroupBranchByDirectoryAction(tree)
152     val toggleFavoriteAction = ToggleFavoriteAction()
153     val fetchAction = FetchAction(this)
154     val showMyBranchesAction = ShowMyBranchesAction(uiController)
155     val newBranchAction = NewBranchAction()
156     val updateSelectedAction = UpdateSelectedBranchAction()
157     val defaultTreeExpander = DefaultTreeExpander(tree.component)
158     val commonActionsManager = CommonActionsManager.getInstance()
159     val expandAllAction = commonActionsManager.createExpandAllHeaderAction(defaultTreeExpander, tree.component)
160     val collapseAllAction = commonActionsManager.createCollapseAllHeaderAction(defaultTreeExpander, tree.component)
161     val actionManager = ActionManager.getInstance()
162     val hideBranchesAction = actionManager.getAction("Git.Log.Hide.Branches")
163     val settings = actionManager.getAction("Git.Log.Branches.Settings")
164
165     val group = DefaultActionGroup()
166     group.add(hideBranchesAction)
167     group.add(Separator())
168     group.add(newBranchAction)
169     group.add(updateSelectedAction)
170     group.add(deleteAction)
171     group.add(diffAction)
172     group.add(showMyBranchesAction)
173     group.add(fetchAction)
174     group.add(toggleFavoriteAction)
175     group.add(Separator())
176     group.add(settings)
177     group.add(groupByDirectoryAction)
178     group.add(expandAllAction)
179     group.add(collapseAllAction)
180
181     val toolbar = actionManager.createActionToolbar("Git.Log.Branches", group, false)
182     toolbar.setTargetComponent(branchesTreePanel)
183
184     val branchesButton = ExpandStripeButton(messagePointer("action.Git.Log.Show.Branches.text"), AllIcons.Actions.ArrowExpand)
185       .apply {
186         border = createBorder(JBColor.border(), SideBorder.RIGHT)
187         addActionListener {
188           if (logUi.properties.exists(SHOW_GIT_BRANCHES_LOG_PROPERTY)) {
189             logUi.properties.set(SHOW_GIT_BRANCHES_LOG_PROPERTY, true)
190           }
191         }
192       }
193     branchesSearchFieldPanel.withBackground(UIUtil.getListBackground()).withBorder(createBorder(JBColor.border(), SideBorder.BOTTOM))
194     branchesSearchFieldPanel.addToCenter(branchesSearchField)
195     branchesTreePanel.addToTop(branchesSearchFieldPanel).addToCenter(branchesProgressStripe)
196     branchesPanelExpandableController = ExpandablePanelController(toolbar.component, branchesButton, branchesTreePanel)
197     branchViewSplitter.firstComponent = branchesTreePanel
198     branchesTreeWithLogPanel.addToLeft(branchesPanelExpandableController.expandControlPanel).addToCenter(branchViewSplitter)
199     mainPanel.isFocusCycleRoot = true
200     mainPanel.focusTraversalPolicy = BRANCHES_UI_FOCUS_TRAVERSAL_POLICY
201     startLoadingBranches()
202   }
203
204   fun toggleBranchesPanelVisibility() {
205     branchesPanelExpandableController.toggleExpand(showBranches)
206     updateBranchesTree(true)
207   }
208
209   private fun createFocusFilterFieldAction(searchField: Component) {
210     DumbAwareAction.create { e ->
211       val project = e.getRequiredData(CommonDataKeys.PROJECT)
212       if (IdeFocusManager.getInstance(project).getFocusedDescendantFor(tree.component) != null) {
213         IdeFocusManager.getInstance(project).requestFocus(searchField, true)
214       }
215       else {
216         IdeFocusManager.getInstance(project).requestFocus(tree.component, true)
217       }
218     }.registerCustomShortcutSet(KeymapUtil.getActiveKeymapShortcuts("Find"), branchesTreePanel)
219   }
220
221   inner class BranchesTreePanel : BorderLayoutPanel(), DataProvider {
222     override fun getData(dataId: String): Any? {
223       return when {
224         GIT_BRANCHES.`is`(dataId) -> tree.getSelectedBranches()
225         GIT_BRANCH_FILTERS.`is`(dataId) -> tree.getSelectedBranchFilters()
226         GIT_REMOTES.`is`(dataId) -> tree.getSelectedRemotes()
227         BRANCHES_UI_CONTROLLER.`is`(dataId) -> uiController
228         VcsLogInternalDataKeys.LOG_UI_PROPERTIES.`is`(dataId) -> logUi.properties
229         else -> null
230       }
231     }
232   }
233
234   fun getMainComponent(): JComponent {
235     return mainPanel
236   }
237
238   fun updateBranchesTree(initial: Boolean) {
239     if (showBranches) {
240       tree.update(initial)
241     }
242   }
243
244   fun refreshTree() {
245     tree.refreshTree()
246   }
247
248   fun startLoadingBranches() {
249     tree.component.emptyText.text = message("action.Git.Loading.Branches.progress")
250     branchesTreePanel.isEnabled = false
251     branchesProgressStripe.startLoading()
252   }
253
254   fun stopLoadingBranches() {
255     tree.component.emptyText.text = getDefaultEmptyText()
256     branchesTreePanel.isEnabled = true
257     branchesProgressStripe.stopLoading()
258   }
259
260   override fun dispose() {
261     disposeBranchesUi()
262   }
263 }
264
265 internal class BranchesVcsLogUiFactory(logManager: VcsLogManager, logId: String, filters: VcsLogFilterCollection? = null)
266   : BaseVcsLogUiFactory<BranchesVcsLogUi>(logId, filters, logManager.uiProperties, logManager.colorManager) {
267   override fun createVcsLogUiImpl(logId: String,
268                                   logData: VcsLogData,
269                                   properties: MainVcsLogUiProperties,
270                                   colorManager: VcsLogColorManager,
271                                   refresher: VisiblePackRefresherImpl,
272                                   filters: VcsLogFilterCollection?) =
273     BranchesVcsLogUi(logId, logData, colorManager, properties, refresher, filters)
274 }
275
276 internal class BranchesVcsLogUi(id: String, logData: VcsLogData, colorManager: VcsLogColorManager,
277                                 uiProperties: MainVcsLogUiProperties, refresher: VisiblePackRefresher,
278                                 initialFilters: VcsLogFilterCollection?) :
279   VcsLogUiImpl(id, logData, colorManager, uiProperties, refresher, initialFilters) {
280
281   private val branchesUi =
282     BranchesDashboardUi(logData.project, this)
283       .also { branchesUi -> Disposer.register(this, branchesUi) }
284
285   internal val mainLogComponent: JComponent
286     get() = mainFrame
287
288   internal val changesBrowser: ChangesBrowserBase
289     get() = mainFrame.changesBrowser
290
291   override fun createMainFrame(logData: VcsLogData, uiProperties: MainVcsLogUiProperties, filterUi: VcsLogFilterUiEx) =
292     MainFrame(logData, this, uiProperties, filterUi, false, this)
293       .apply {
294         isFocusCycleRoot = false
295         focusTraversalPolicy = null //new focus traversal policy will be configured include branches tree
296         if (isDiffPreviewInEditor()) {
297           VcsLogEditorDiffPreview(myProject, uiProperties, this)
298         }
299       }
300
301   override fun getMainComponent() = branchesUi.getMainComponent()
302
303   fun createDiffPreview(isInEditor: Boolean): VcsLogChangeProcessor {
304     return mainFrame.createDiffPreview(isInEditor, mainFrame.changesBrowser)
305   }
306 }
307
308 internal val SHOW_GIT_BRANCHES_LOG_PROPERTY =
309   object : VcsLogProjectTabsProperties.CustomBooleanTabProperty("Show.Git.Branches") {
310     override fun defaultValue(logId: String) = logId == MAIN_LOG_ID
311   }
312
313 internal val CHANGE_LOG_FILTER_ON_BRANCH_SELECTION_PROPERTY =
314   object : VcsLogApplicationSettings.CustomBooleanProperty("Change.Log.Filter.on.Branch.Selection") {
315     override fun defaultValue() = false
316   }
317
318 private class BranchViewSplitter(first: JComponent? = null, second: JComponent? = null)
319   : OnePixelSplitter(false, "vcs.branch.view.splitter.proportion", 0.3f) {
320   init {
321     firstComponent = first
322     secondComponent = second
323   }
324 }
325
326 private class DiffPreviewSplitter(diffPreview: VcsLogChangeProcessor, uiProperties: VcsLogUiProperties, mainComponent: JComponent)
327   : FrameDiffPreview<VcsLogChangeProcessor>(diffPreview, uiProperties, mainComponent,
328                                             "vcs.branch.view.diff.splitter.proportion",
329                                             uiProperties[MainVcsLogUiProperties.DIFF_PREVIEW_VERTICAL_SPLIT], 0.3f) {
330   override fun updatePreview(state: Boolean) {
331     previewDiff.updatePreview(state)
332   }
333 }