37c3d1ab3efe7bd4f78536861ce5a3d765a7cc80
[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         BRANCHES_UI_CONTROLLER.`is`(dataId) -> uiController
227         VcsLogInternalDataKeys.LOG_UI_PROPERTIES.`is`(dataId) -> logUi.properties
228         else -> null
229       }
230     }
231   }
232
233   fun getMainComponent(): JComponent {
234     return mainPanel
235   }
236
237   fun updateBranchesTree(initial: Boolean) {
238     if (showBranches) {
239       tree.update(initial)
240     }
241   }
242
243   fun refreshTree() {
244     tree.refreshTree()
245   }
246
247   fun startLoadingBranches() {
248     tree.component.emptyText.text = message("action.Git.Loading.Branches.progress")
249     branchesTreePanel.isEnabled = false
250     branchesProgressStripe.startLoading()
251   }
252
253   fun stopLoadingBranches() {
254     tree.component.emptyText.text = getDefaultEmptyText()
255     branchesTreePanel.isEnabled = true
256     branchesProgressStripe.stopLoading()
257   }
258
259   override fun dispose() {
260     disposeBranchesUi()
261   }
262 }
263
264 internal class BranchesVcsLogUiFactory(logManager: VcsLogManager, logId: String, filters: VcsLogFilterCollection? = null)
265   : BaseVcsLogUiFactory<BranchesVcsLogUi>(logId, filters, logManager.uiProperties, logManager.colorManager) {
266   override fun createVcsLogUiImpl(logId: String,
267                                   logData: VcsLogData,
268                                   properties: MainVcsLogUiProperties,
269                                   colorManager: VcsLogColorManager,
270                                   refresher: VisiblePackRefresherImpl,
271                                   filters: VcsLogFilterCollection?) =
272     BranchesVcsLogUi(logId, logData, colorManager, properties, refresher, filters)
273 }
274
275 internal class BranchesVcsLogUi(id: String, logData: VcsLogData, colorManager: VcsLogColorManager,
276                                 uiProperties: MainVcsLogUiProperties, refresher: VisiblePackRefresher,
277                                 initialFilters: VcsLogFilterCollection?) :
278   VcsLogUiImpl(id, logData, colorManager, uiProperties, refresher, initialFilters) {
279
280   private val branchesUi =
281     BranchesDashboardUi(logData.project, this)
282       .also { branchesUi -> Disposer.register(this, branchesUi) }
283
284   internal val mainLogComponent: JComponent
285     get() = mainFrame
286
287   internal val changesBrowser: ChangesBrowserBase
288     get() = mainFrame.changesBrowser
289
290   override fun createMainFrame(logData: VcsLogData, uiProperties: MainVcsLogUiProperties, filterUi: VcsLogFilterUiEx) =
291     MainFrame(logData, this, uiProperties, filterUi, false, this)
292       .apply {
293         isFocusCycleRoot = false
294         focusTraversalPolicy = null //new focus traversal policy will be configured include branches tree
295         if (isDiffPreviewInEditor()) {
296           VcsLogEditorDiffPreview(myProject, uiProperties, this)
297         }
298       }
299
300   override fun getMainComponent() = branchesUi.getMainComponent()
301
302   fun createDiffPreview(isInEditor: Boolean): VcsLogChangeProcessor {
303     return mainFrame.createDiffPreview(isInEditor, mainFrame.changesBrowser)
304   }
305 }
306
307 internal val SHOW_GIT_BRANCHES_LOG_PROPERTY =
308   object : VcsLogProjectTabsProperties.CustomBooleanTabProperty("Show.Git.Branches") {
309     override fun defaultValue(logId: String) = logId == MAIN_LOG_ID
310   }
311
312 internal val CHANGE_LOG_FILTER_ON_BRANCH_SELECTION_PROPERTY =
313   object : VcsLogApplicationSettings.CustomBooleanProperty("Change.Log.Filter.on.Branch.Selection") {
314     override fun defaultValue() = false
315   }
316
317 private class BranchViewSplitter(first: JComponent? = null, second: JComponent? = null)
318   : OnePixelSplitter(false, "vcs.branch.view.splitter.proportion", 0.3f) {
319   init {
320     firstComponent = first
321     secondComponent = second
322   }
323 }
324
325 private class DiffPreviewSplitter(diffPreview: VcsLogChangeProcessor, uiProperties: VcsLogUiProperties, mainComponent: JComponent)
326   : FrameDiffPreview<VcsLogChangeProcessor>(diffPreview, uiProperties, mainComponent,
327                                             "vcs.branch.view.diff.splitter.proportion",
328                                             uiProperties[MainVcsLogUiProperties.DIFF_PREVIEW_VERTICAL_SPLIT], 0.3f) {
329   override fun updatePreview(state: Boolean) {
330     previewDiff.updatePreview(state)
331   }
332 }