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 com.intellij.vcs.commit
4 import com.intellij.openapi.Disposable
5 import com.intellij.openapi.actionSystem.*
6 import com.intellij.openapi.editor.colors.EditorColorsListener
7 import com.intellij.openapi.editor.colors.EditorColorsScheme
8 import com.intellij.openapi.editor.ex.EditorEx
9 import com.intellij.openapi.project.DumbAwareAction
10 import com.intellij.openapi.ui.ComponentContainer
11 import com.intellij.openapi.ui.Messages
12 import com.intellij.openapi.ui.popup.JBPopup
13 import com.intellij.openapi.ui.popup.JBPopupFactory
14 import com.intellij.openapi.ui.popup.JBPopupListener
15 import com.intellij.openapi.ui.popup.LightweightWindowEvent
16 import com.intellij.openapi.util.Disposer
17 import com.intellij.openapi.util.SystemInfo.isMac
18 import com.intellij.openapi.vcs.FilePath
19 import com.intellij.openapi.vcs.VcsBundle.message
20 import com.intellij.openapi.vcs.changes.*
21 import com.intellij.openapi.vcs.changes.ui.ChangesBrowserNode
22 import com.intellij.openapi.vcs.changes.ui.ChangesBrowserNode.UNVERSIONED_FILES_TAG
23 import com.intellij.openapi.vcs.changes.ui.ChangesViewContentManager
24 import com.intellij.openapi.vcs.changes.ui.ChangesViewContentManager.Companion.LOCAL_CHANGES
25 import com.intellij.openapi.vcs.changes.ui.ChangesViewContentManager.Companion.getToolWindowFor
26 import com.intellij.openapi.vcs.changes.ui.EditChangelistSupport
27 import com.intellij.openapi.vcs.changes.ui.VcsTreeModelData.*
28 import com.intellij.openapi.vcs.checkin.CheckinHandler
29 import com.intellij.openapi.vcs.ui.CommitMessage
30 import com.intellij.openapi.wm.IdeFocusManager
31 import com.intellij.openapi.wm.ToolWindow
32 import com.intellij.ui.IdeBorderFactory.createBorder
33 import com.intellij.ui.JBColor
34 import com.intellij.ui.SideBorder
35 import com.intellij.ui.awt.RelativePoint
36 import com.intellij.ui.components.JBOptionButton
37 import com.intellij.ui.components.JBOptionButton.Companion.getDefaultShowPopupShortcut
38 import com.intellij.ui.components.JBPanel
39 import com.intellij.ui.components.panels.VerticalLayout
40 import com.intellij.util.EventDispatcher
41 import com.intellij.util.IJSwingUtilities.updateComponentTreeUI
42 import com.intellij.util.ui.JBUI.Borders.empty
43 import com.intellij.util.ui.JBUI.Borders.emptyLeft
44 import com.intellij.util.ui.JBUI.Panels.simplePanel
45 import com.intellij.util.ui.JBUI.scale
46 import com.intellij.util.ui.UIUtil.getTreeBackground
47 import com.intellij.util.ui.components.BorderLayoutPanel
48 import com.intellij.util.ui.tree.TreeUtil.*
49 import com.intellij.vcs.log.VcsUser
50 import java.awt.LayoutManager
52 import java.awt.event.ActionEvent
53 import java.awt.event.InputEvent
54 import java.awt.event.KeyEvent
56 import javax.swing.KeyStroke.getKeyStroke
57 import javax.swing.border.Border
58 import javax.swing.border.EmptyBorder
59 import kotlin.properties.Delegates.observable
61 private val CTRL_ENTER = KeyboardShortcut(getKeyStroke(KeyEvent.VK_ENTER, InputEvent.CTRL_DOWN_MASK), null)
62 private val META_ENTER = KeyboardShortcut(getKeyStroke(KeyEvent.VK_ENTER, InputEvent.META_DOWN_MASK), null)
63 private val DEFAULT_COMMIT_ACTION_SHORTCUT: ShortcutSet =
64 if (isMac) CustomShortcutSet(CTRL_ENTER, META_ENTER) else CustomShortcutSet(CTRL_ENTER)
65 fun getDefaultCommitShortcut() = DEFAULT_COMMIT_ACTION_SHORTCUT
67 private fun panel(layout: LayoutManager): JBPanel<*> = JBPanel<JBPanel<*>>(layout)
69 fun showEmptyCommitMessageConfirmation() = Messages.YES == Messages.showYesNoDialog(
70 message("confirmation.text.check.in.with.empty.comment"),
71 message("confirmation.title.check.in.with.empty.comment"),
72 Messages.getWarningIcon()
75 fun JBOptionButton.getBottomInset(): Int =
76 border?.getBorderInsets(this)?.bottom
77 ?: (components.firstOrNull() as? JComponent)?.insets?.bottom
80 private fun JBPopup.showAbove(component: JComponent) {
81 val northWest = RelativePoint(component, Point())
83 addListener(object : JBPopupListener {
84 override fun beforeShown(event: LightweightWindowEvent) {
85 val popup = event.asPopup()
86 val location = Point(popup.locationOnScreen).apply { y = northWest.screenPoint.y - popup.size.height }
88 popup.setLocation(location)
94 internal fun ChangesBrowserNode<*>.subtreeRootObject(): Any? = (path.getOrNull(1) as? ChangesBrowserNode<*>)?.userObject
96 class ChangesViewCommitPanel(private val changesViewHost: ChangesViewPanel, private val rootComponent: JComponent) :
97 BorderLayoutPanel(), ChangesViewCommitWorkflowUi, EditorColorsListener, ComponentContainer, DataProvider {
99 private val changesView get() = changesViewHost.changesView
100 private val project get() = changesView.project
102 private val dataProviders = mutableListOf<DataProvider>()
104 private val executorEventDispatcher = EventDispatcher.create(CommitExecutorListener::class.java)
105 private val inclusionEventDispatcher = EventDispatcher.create(InclusionListener::class.java)
107 private val centerPanel = simplePanel()
108 private val buttonPanel = simplePanel().apply { isOpaque = false }
109 private val toolbarPanel = simplePanel().apply {
111 border = emptyLeft(1)
113 private val actions = ActionManager.getInstance().getAction("ChangesView.CommitToolbar") as ActionGroup
114 private val toolbar = ActionManager.getInstance().createActionToolbar(COMMIT_TOOLBAR_PLACE, actions, false).apply {
115 setTargetComponent(this@ChangesViewCommitPanel)
116 component.isOpaque = false
119 private val commitMessage = CommitMessage(project, false, false, true).apply {
120 editorField.addSettingsProvider { it.setBorder(emptyLeft(6)) }
121 editorField.setPlaceholder(message("commit.message.placeholder"))
123 private val defaultCommitAction = object : AbstractAction() {
124 override fun actionPerformed(e: ActionEvent) = fireDefaultExecutorCalled()
126 private val commitButton = object : JBOptionButton(defaultCommitAction, emptyArray()) {
128 background = getButtonPanelBackground()
129 optionTooltipText = getDefaultTooltip()
130 isOkToProcessDefaultMnemonics = false
133 override fun isDefaultButton(): Boolean = IdeFocusManager.getInstance(project).getFocusedDescendantFor(rootComponent) != null
135 private val commitAuthorComponent = CommitAuthorComponent(project)
137 private var needUpdateCommitOptionsUi = false
139 private var isHideToolWindowOnDeactivate = false
141 var isToolbarHorizontal: Boolean by observable(false) { _, oldValue, newValue ->
142 if (oldValue != newValue) {
143 addToolbar(newValue) // this also removes toolbar from previous parent
148 Disposer.register(this, commitMessage)
151 for (support in EditChangelistSupport.EP_NAME.getExtensions(project)) {
152 support.installSearch(commitMessage.editorField, commitMessage.editorField)
156 setInclusionListener { inclusionEventDispatcher.multicaster.inclusionChanged() }
157 isShowCheckboxes = true
159 changesViewHost.statusComponent =
160 ChangesViewCommitStatusPanel(changesView, this).apply { addToLeft(toolbarPanel) }
162 setupShortcuts(rootComponent)
165 private fun buildLayout() {
167 border = getButtonPanelBorder()
169 addToLeft(commitButton)
172 .addToCenter(commitMessage)
173 .addToBottom(panel(VerticalLayout(0)).apply {
174 background = getButtonPanelBackground()
176 add(commitAuthorComponent.apply { border = empty(0, 5, 4, 0) })
179 addToCenter(centerPanel)
180 addToolbar(isToolbarHorizontal)
182 withPreferredHeight(85)
185 private fun addToolbar(isHorizontal: Boolean) {
187 toolbar.setOrientation(SwingConstants.HORIZONTAL)
188 toolbar.setReservePlaceAutoPopupIcon(false)
190 centerPanel.border = null
191 toolbarPanel.addToCenter(toolbar.component)
194 toolbar.setOrientation(SwingConstants.VERTICAL)
195 toolbar.setReservePlaceAutoPopupIcon(true)
197 centerPanel.border = createBorder(JBColor.border(), SideBorder.LEFT)
198 addToLeft(toolbar.component)
202 private fun getButtonPanelBorder(): Border =
203 EmptyBorder(0, scale(3), (scale(6) - commitButton.getBottomInset()).coerceAtLeast(0), 0)
205 private fun getButtonPanelBackground() =
206 JBColor { (commitMessage.editorField.editor as? EditorEx)?.backgroundColor ?: getTreeBackground() }
208 private fun fireDefaultExecutorCalled() = executorEventDispatcher.multicaster.executorCalled(null)
210 private fun setupShortcuts(component: JComponent) {
211 DefaultCommitAction().registerCustomShortcutSet(DEFAULT_COMMIT_ACTION_SHORTCUT, component, this)
212 ShowCustomCommitActions().registerCustomShortcutSet(getDefaultShowPopupShortcut(), component, this)
215 override fun globalSchemeChange(scheme: EditorColorsScheme?) {
216 needUpdateCommitOptionsUi = true
217 buttonPanel.border = getButtonPanelBorder()
220 override val commitMessageUi: CommitMessageUi get() = commitMessage
222 // NOTE: getter should return text with mnemonic (if any) to make mnemonics available in dialogs shown by commit handlers.
223 // See CheckinProjectPanel.getCommitActionName() usages.
224 override var defaultCommitActionName: String
225 get() = (defaultCommitAction.getValue(Action.NAME) as? String).orEmpty()
226 set(value) = defaultCommitAction.putValue(Action.NAME, value)
228 override var isDefaultCommitActionEnabled: Boolean
229 get() = defaultCommitAction.isEnabled
231 defaultCommitAction.isEnabled = value
234 override fun setCustomCommitActions(actions: List<AnAction>) = commitButton.setOptions(actions)
236 override var commitAuthor: VcsUser?
237 get() = commitAuthorComponent.commitAuthor
239 commitAuthorComponent.commitAuthor = value
242 override fun addCommitAuthorListener(listener: CommitAuthorListener, parent: Disposable) =
243 commitAuthorComponent.addCommitAuthorListener(listener, parent)
245 override var editedCommit by observable<EditedCommitDetails?>(null) { _, _, newValue ->
247 newValue?.let { expand(it) }
250 override val isActive: Boolean get() = isVisible
252 override fun activate(): Boolean {
253 val toolWindow = getVcsToolWindow() ?: return false
254 val contentManager = ChangesViewContentManager.getInstance(project)
256 saveToolWindowState()
257 changesView.isShowCheckboxes = true
260 contentManager.selectContent(LOCAL_CHANGES)
261 toolWindow.activate({ commitMessage.requestFocusInMessage() }, false)
265 override fun deactivate(isRestoreState: Boolean) {
266 if (isRestoreState) restoreToolWindowState()
267 clearToolWindowState()
268 changesView.isShowCheckboxes = false
272 private fun saveToolWindowState() {
274 isHideToolWindowOnDeactivate = getVcsToolWindow()?.isVisible != true
278 private fun restoreToolWindowState() {
279 if (isHideToolWindowOnDeactivate) {
280 getVcsToolWindow()?.hide(null)
284 private fun clearToolWindowState() {
285 isHideToolWindowOnDeactivate = false
288 private fun getVcsToolWindow(): ToolWindow? = getToolWindowFor(project, LOCAL_CHANGES)
290 override fun expand(item: Any) {
291 val node = changesView.findNodeInTree(item)
292 node?.let { changesView.expandSafe(it) }
295 override fun select(item: Any) {
296 val path = changesView.findNodePathInTree(item)
297 path?.let { selectPath(changesView, it, false) }
300 override fun selectFirst(items: Collection<Any>) {
301 if (items.isEmpty()) return
303 val path = treePathTraverser(changesView).preOrderDfsTraversal().find { getLastUserObject(it) in items }
304 path?.let { selectPath(changesView, it, false) }
307 override fun showCommitOptions(options: CommitOptions, actionName: String, isFromToolbar: Boolean, dataContext: DataContext) {
308 val commitOptionsPanel = CommitOptionsPanel { actionName }.apply {
309 focusTraversalPolicy = LayoutFocusTraversalPolicy()
310 isFocusCycleRoot = true
313 border = empty(0, 10)
315 // to reflect LaF changes as commit options components are created once per commit
316 if (needUpdateCommitOptionsUi) {
317 needUpdateCommitOptionsUi = false
318 updateComponentTreeUI(this)
321 val focusComponent = IdeFocusManager.getInstance(project).getFocusTargetFor(commitOptionsPanel)
322 val commitOptionsPopup = JBPopupFactory.getInstance()
323 .createComponentPopupBuilder(commitOptionsPanel, focusComponent)
324 .setRequestFocus(true)
327 commitOptionsPopup.show(isFromToolbar, dataContext)
330 private fun JBPopup.show(isFromToolbar: Boolean, dataContext: DataContext) =
332 isFromToolbar && isToolbarHorizontal -> showAbove(toolbar.component)
333 isFromToolbar && !isToolbarHorizontal -> showAbove(this@ChangesViewCommitPanel)
334 else -> showInBestPositionFor(dataContext)
337 override fun setCompletionContext(changeLists: List<LocalChangeList>) {
338 commitMessage.changeLists = changeLists
341 override fun getComponent(): JComponent = this
342 override fun getPreferredFocusableComponent(): JComponent = commitMessage.editorField
344 override fun getData(dataId: String) = getDataFromProviders(dataId) ?: commitMessage.getData(dataId)
345 fun getDataFromProviders(dataId: String) = dataProviders.asSequence().mapNotNull { it.getData(dataId) }.firstOrNull()
347 override fun addDataProvider(provider: DataProvider) {
348 dataProviders += provider
351 override fun addExecutorListener(listener: CommitExecutorListener, parent: Disposable) =
352 executorEventDispatcher.addListener(listener, parent)
354 override fun refreshData() = ChangesViewManager.getInstanceEx(project).refreshImmediately()
356 override fun getDisplayedChanges(): List<Change> = all(changesView).userObjects(Change::class.java)
357 override fun getIncludedChanges(): List<Change> = included(changesView).userObjects(Change::class.java)
359 override fun getDisplayedUnversionedFiles(): List<FilePath> =
360 allUnderTag(changesView, UNVERSIONED_FILES_TAG).userObjects(FilePath::class.java)
362 override fun getIncludedUnversionedFiles(): List<FilePath> =
363 includedUnderTag(changesView, UNVERSIONED_FILES_TAG).userObjects(FilePath::class.java)
365 override var inclusionModel: InclusionModel?
366 get() = changesView.inclusionModel
368 changesView.setInclusionModel(value)
371 override fun includeIntoCommit(items: Collection<*>) = changesView.includeChanges(items)
373 override fun addInclusionListener(listener: InclusionListener, parent: Disposable) =
374 inclusionEventDispatcher.addListener(listener, parent)
376 override fun confirmCommitWithEmptyMessage(): Boolean = showEmptyCommitMessageConfirmation()
378 override fun startBeforeCommitChecks() = Unit
379 override fun endBeforeCommitChecks(result: CheckinHandler.ReturnResult) = Unit
381 override fun endExecution() = closeEditorPreviewIfEmpty()
383 private fun closeEditorPreviewIfEmpty() {
384 val changesViewManager = ChangesViewManager.getInstance(project) as? ChangesViewManager ?: return
385 if (!changesViewManager.isEditorPreview) return
388 changesViewManager.closeEditorPreview(true)
391 override fun dispose() {
392 changesViewHost.statusComponent = null
394 isShowCheckboxes = false
395 setInclusionListener(null)
399 inner class DefaultCommitAction : DumbAwareAction() {
400 override fun update(e: AnActionEvent) {
401 e.presentation.isEnabledAndVisible = isActive && defaultCommitAction.isEnabled
404 override fun actionPerformed(e: AnActionEvent) = fireDefaultExecutorCalled()
407 private inner class ShowCustomCommitActions : DumbAwareAction() {
408 override fun update(e: AnActionEvent) {
409 e.presentation.isEnabledAndVisible = isActive && commitButton.isEnabled
412 override fun actionPerformed(e: AnActionEvent) = commitButton.showPopup()
416 internal const val COMMIT_TOOLBAR_PLACE: String = "ChangesView.CommitToolbar"