1 // Copyright 2000-2021 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.openapi.vcs.changes
4 import com.intellij.diff.chains.DiffRequestChain
5 import com.intellij.diff.chains.SimpleDiffRequestChain
6 import com.intellij.diff.editor.DiffVirtualFile
7 import com.intellij.diff.impl.DiffRequestProcessor
8 import com.intellij.diff.util.DiffUserDataKeysEx
9 import com.intellij.ide.actions.SplitAction
10 import com.intellij.openapi.Disposable
11 import com.intellij.openapi.ListSelection
12 import com.intellij.openapi.actionSystem.ActionManager
13 import com.intellij.openapi.actionSystem.AnActionEvent
14 import com.intellij.openapi.actionSystem.IdeActions
15 import com.intellij.openapi.fileEditor.FileEditor
16 import com.intellij.openapi.fileEditor.FileEditorManager
17 import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
18 import com.intellij.openapi.project.DumbAwareAction
19 import com.intellij.openapi.project.DumbService
20 import com.intellij.openapi.project.Project
21 import com.intellij.openapi.util.Disposer
22 import com.intellij.openapi.util.Disposer.isDisposed
23 import com.intellij.openapi.vcs.changes.ui.ChangesTree
24 import com.intellij.openapi.vfs.VirtualFile
25 import com.intellij.openapi.wm.ToolWindowManager
26 import com.intellij.util.EditSourceOnDoubleClickHandler.isToggleEvent
27 import com.intellij.util.IJSwingUtilities
28 import com.intellij.util.Processor
29 import com.intellij.util.ui.update.DisposableUpdate
30 import com.intellij.util.ui.update.MergingUpdateQueue
31 import org.jetbrains.annotations.Nls
32 import java.awt.event.KeyEvent
33 import java.awt.event.MouseEvent
34 import javax.swing.JComponent
35 import kotlin.streams.toList
37 abstract class EditorTabPreview(protected val diffProcessor: DiffRequestProcessor) : DiffPreview {
38 protected val project get() = diffProcessor.project!!
39 private val previewFile = EditorTabDiffPreviewVirtualFile(this)
40 private val updatePreviewQueue =
41 MergingUpdateQueue("updatePreviewQueue", 100, true, null, diffProcessor).apply {
42 setRestartTimerOnAdd(true)
44 private val updatePreviewProcessor: DiffPreviewUpdateProcessor? get() = diffProcessor as? DiffPreviewUpdateProcessor
46 var escapeHandler: Runnable? = null
48 fun openWithDoubleClick(tree: ChangesTree) {
49 installDoubleClickHandler(tree)
50 installEnterKeyHandler(tree)
51 installSelectionChangedHandler(tree) { updatePreview(false) }
54 fun openWithSingleClick(tree: ChangesTree) {
55 //do not open file aggressively on start up, do it later
56 DumbService.getInstance(project).smartInvokeLater {
57 if (isDisposed(updatePreviewQueue)) return@smartInvokeLater
59 installSelectionChangedHandler(tree) {
60 if (!openPreview(false)) closePreview() // auto-close editor tab if nothing to preview
65 fun installNextDiffActionOn(component: JComponent) {
66 DumbAwareAction.create { openPreview(true) }.apply {
67 copyShortcutFrom(ActionManager.getInstance().getAction(IdeActions.ACTION_NEXT_DIFF))
68 registerCustomShortcutSet(component, diffProcessor)
72 protected open fun isPreviewOnDoubleClickAllowed(): Boolean = true
73 protected open fun isPreviewOnEnterAllowed(): Boolean = true
75 private fun installDoubleClickHandler(tree: ChangesTree) {
76 val oldDoubleClickHandler = tree.doubleClickHandler
77 val newDoubleClickHandler = Processor<MouseEvent> { e ->
78 if (isToggleEvent(tree, e)) return@Processor false
80 isPreviewOnDoubleClickAllowed() && openPreview(true) || oldDoubleClickHandler?.process(e) == true
83 tree.doubleClickHandler = newDoubleClickHandler
84 Disposer.register(diffProcessor, Disposable { tree.doubleClickHandler = oldDoubleClickHandler })
87 private fun installEnterKeyHandler(tree: ChangesTree) {
88 val oldEnterKeyHandler = tree.enterKeyHandler
89 val newEnterKeyHandler = Processor<KeyEvent> { e ->
90 isPreviewOnEnterAllowed() && openPreview(false) || oldEnterKeyHandler?.process(e) == true
93 tree.enterKeyHandler = newEnterKeyHandler
94 Disposer.register(diffProcessor, Disposable { tree.enterKeyHandler = oldEnterKeyHandler })
97 private fun installSelectionChangedHandler(tree: ChangesTree, handler: () -> Unit) =
98 tree.addSelectionListener(
100 updatePreviewQueue.queue(DisposableUpdate.createDisposable(updatePreviewQueue, this) {
101 if (!skipPreviewUpdate()) handler()
107 protected abstract fun getCurrentName(): String?
109 protected abstract fun hasContent(): Boolean
111 protected open fun skipPreviewUpdate(): Boolean = ToolWindowManager.getInstance(project).isEditorComponentActive
113 override fun updatePreview(fromModelRefresh: Boolean) {
114 if (isPreviewOpen()) {
115 updatePreviewProcessor?.refresh(false)
116 FileEditorManagerEx.getInstanceEx(project).updateFilePresentation(previewFile)
119 updatePreviewProcessor?.clear()
123 override fun setPreviewVisible(isPreviewVisible: Boolean, focus: Boolean) {
124 if (isPreviewVisible) openPreview(focus) else closePreview()
127 private fun isPreviewOpen(): Boolean = FileEditorManager.getInstance(project).isFileOpen(previewFile)
130 FileEditorManager.getInstance(project).closeFile(previewFile)
131 updatePreviewProcessor?.clear()
134 fun openPreview(focusEditor: Boolean): Boolean {
135 updatePreviewProcessor?.refresh(false)
136 if (!hasContent()) return false
138 escapeHandler?.let { handler -> registerEscapeHandler(previewFile, handler) }
140 openPreview(project, previewFile, focusEditor)
145 private class EditorTabDiffPreviewVirtualFile(val preview: EditorTabPreview)
146 : PreviewDiffVirtualFile(EditorTabDiffPreviewProvider(preview.diffProcessor) { preview.getCurrentName() }) {
148 // EditorTabDiffPreviewProvider does not create new processor, so general assumptions of DiffVirtualFile are violated
149 preview.diffProcessor.putContextUserData(DiffUserDataKeysEx.DIFF_IN_EDITOR_WITH_EXPLICIT_DISPOSABLE, true)
150 putUserData(SplitAction.FORBID_TAB_SPLIT, true)
155 fun openPreview(project: Project, file: PreviewDiffVirtualFile, focusEditor: Boolean): Array<out FileEditor> {
156 return VcsEditorTabFilesManager.getInstance().openFile(project, file, focusEditor)
159 fun registerEscapeHandler(file: VirtualFile, handler: Runnable) {
160 file.putUserData(DiffVirtualFile.ESCAPE_HANDLER, EditorTabPreviewEscapeAction(handler))
165 internal class EditorTabPreviewEscapeAction(private val escapeHandler: Runnable) : DumbAwareAction() {
166 override fun actionPerformed(e: AnActionEvent) = escapeHandler.run()
169 private class EditorTabDiffPreviewProvider(
170 private val diffProcessor: DiffRequestProcessor,
171 private val tabNameProvider: () -> String?
172 ) : ChainBackedDiffPreviewProvider {
173 override fun createDiffRequestProcessor(): DiffRequestProcessor {
174 IJSwingUtilities.updateComponentTreeUI(diffProcessor.component)
178 override fun getOwner(): Any = this
180 override fun getEditorTabName(): @Nls String = tabNameProvider().orEmpty()
182 override fun createDiffRequestChain(): DiffRequestChain? {
183 if (diffProcessor is ChangeViewDiffRequestProcessor) {
184 val selection = ListSelection.create(diffProcessor.allChanges.toList(), diffProcessor.currentChange)
185 val producers = selection.map { it!!.createProducer(diffProcessor.project) }
186 val chain = SimpleDiffRequestChain.fromProducers(producers.list)
187 chain.index = producers.selectedIndex