summaryrefslogtreecommitdiff
path: root/platform/vcs-log/impl/src/com/intellij/vcs/log/ui/details/commit/CommitDetailsPanel.kt
blob: bfcf7e99b2e94b5af1269f97591994356f5711fb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
// Copyright 2000-2019 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.
package com.intellij.vcs.log.ui.details.commit

import com.intellij.ide.IdeTooltipManager
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.actionSystem.impl.ActionToolbarImpl
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.ui.VerticalFlowLayout
import com.intellij.openapi.ui.popup.Balloon
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.util.text.HtmlChunk
import com.intellij.openapi.vcs.ui.FontUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.ui.BrowserHyperlinkListener
import com.intellij.ui.ColorUtil
import com.intellij.ui.components.panels.Wrapper
import com.intellij.ui.scale.JBUIScale
import com.intellij.util.ui.*
import com.intellij.util.ui.components.BorderLayoutPanel
import com.intellij.vcs.log.CommitId
import com.intellij.vcs.log.VcsRef
import com.intellij.vcs.log.ui.frame.CommitPresentationUtil.*
import com.intellij.vcs.log.ui.frame.VcsCommitExternalStatusPresentation
import com.intellij.vcs.log.util.VcsLogUiUtil
import net.miginfocom.layout.CC
import net.miginfocom.layout.LC
import net.miginfocom.swing.MigLayout
import java.awt.*
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.JPanel
import javax.swing.event.HyperlinkEvent

class CommitDetailsPanel @JvmOverloads constructor(navigate: (CommitId) -> Unit = {}) : JPanel() {
  companion object {
    const val SIDE_BORDER = 14
    const val INTERNAL_BORDER = 10
    const val EXTERNAL_BORDER = 14
  }

  private val statusesActionGroup = DefaultActionGroup()

  data class RootColor(val root: VirtualFile, val color: Color)

  private val hashAndAuthorPanel = HashAndAuthorPanel()
  private val statusesToolbar = ActionManager.getInstance().createActionToolbar("CommitDetailsPanel", statusesActionGroup, false).apply {
    targetComponent = this@CommitDetailsPanel
    (this as ActionToolbarImpl).setForceShowFirstComponent(true)
    component.apply {
      isOpaque = false
      border = JBUI.Borders.empty()
      isVisible = false
    }
  }
  private val messagePanel = CommitMessagePanel(navigate)
  private val branchesPanel = ReferencesPanel(Registry.intValue("vcs.log.max.branches.shown"))
  private val tagsPanel = ReferencesPanel(Registry.intValue("vcs.log.max.tags.shown"))
  private val rootPanel = RootColorPanel(hashAndAuthorPanel)
  private val containingBranchesPanel = ContainingBranchesPanel()

  init {
    layout = MigLayout(LC().gridGap("0", "0").insets("0").fill())
    isOpaque = false

    val mainPanel = JPanel(null).apply {
      layout = VerticalFlowLayout(VerticalFlowLayout.TOP, 0, 0, true, false)
      isOpaque = false

      val metadataPanel = BorderLayoutPanel().apply {
        isOpaque = false
        border = JBUI.Borders.empty(INTERNAL_BORDER, SIDE_BORDER, INTERNAL_BORDER, 0)
        addToLeft(rootPanel)
        addToCenter(hashAndAuthorPanel)
      }

      add(messagePanel)
      add(metadataPanel)
      add(branchesPanel)
      add(tagsPanel)
      add(containingBranchesPanel)
    }

    add(mainPanel, CC().grow().push())
    //show at most 4 icons
    val maxHeight = JBUIScale.scale(22 * 4)
    add(statusesToolbar.component, CC().hideMode(3).alignY("top").maxHeight("$maxHeight"))

    updateStatusToolbar(false)
  }

  fun setCommit(presentation: CommitPresentation) {
    messagePanel.updateMessage(presentation)
    hashAndAuthorPanel.presentation = presentation
  }

  fun setRefs(references: List<VcsRef>?) {
    references ?: return
    branchesPanel.setReferences(references.filter { it.type.isBranch })
    tagsPanel.setReferences(references.filter { !it.type.isBranch })
    if (tagsPanel.isVisible) {
      branchesPanel.border = JBUI.Borders.empty(0, SIDE_BORDER - ReferencesPanel.H_GAP, 0, 0)
      tagsPanel.border = JBUI.Borders.empty(0, SIDE_BORDER - ReferencesPanel.H_GAP, INTERNAL_BORDER, 0)
    }
    else if (branchesPanel.isVisible) {
      branchesPanel.border = JBUI.Borders.empty(0, SIDE_BORDER - ReferencesPanel.H_GAP, INTERNAL_BORDER, 0)
    }
    update()
  }

  fun setRoot(rootColor: RootColor?) {
    rootPanel.setRoot(rootColor)
  }

  fun setBranches(branches: List<String>?) {
    containingBranchesPanel.setBranches(branches)
  }

  fun setStatuses(statuses: List<VcsCommitExternalStatusPresentation>) {
    hashAndAuthorPanel.signature = statuses.filterIsInstance(VcsCommitExternalStatusPresentation.Signature::class.java).firstOrNull()

    val nonSignaturesStatuses = statuses.filter { it !is VcsCommitExternalStatusPresentation.Signature }

    statusesActionGroup.removeAll()
    statusesActionGroup.addAll(nonSignaturesStatuses.map(::statusToAction))

    updateStatusToolbar(nonSignaturesStatuses.isNotEmpty())
  }

  private fun statusToAction(status: VcsCommitExternalStatusPresentation) =
    object : DumbAwareAction(status.text, null, status.icon) {
      override fun update(e: AnActionEvent) {
        e.presentation.apply {
          isVisible = true
          isEnabled = status is VcsCommitExternalStatusPresentation.Clickable && status.clickEnabled(e.inputEvent)
          disabledIcon = status.icon
        }
      }

      override fun actionPerformed(e: AnActionEvent) {
        if (status is VcsCommitExternalStatusPresentation.Clickable) {
          if (status.clickEnabled(e.inputEvent))
            status.onClick(e.inputEvent)
        }
      }
    }

  private fun updateStatusToolbar(hasStatuses: Boolean) {
    border = if (hasStatuses) JBUI.Borders.empty() else JBUI.Borders.emptyRight(SIDE_BORDER)
    statusesToolbar.updateActionsImmediately()
    statusesToolbar.component.isVisible = hasStatuses
  }

  fun update() {
    messagePanel.update()
    rootPanel.update()
    hashAndAuthorPanel.update()
    branchesPanel.update()
    tagsPanel.update()
    containingBranchesPanel.update()
  }

  override fun getBackground(): Color = getCommitDetailsBackground()
}

private class CommitMessagePanel(private val navigate: (CommitId) -> Unit) : HtmlPanel() {
  private var presentation: CommitPresentation? = null

  override fun hyperlinkUpdate(e: HyperlinkEvent) {
    presentation?.let { presentation ->
      if (e.eventType == HyperlinkEvent.EventType.ACTIVATED && isGoToHash(e)) {
        val commitId = presentation.parseTargetCommit(e) ?: return
        navigate(commitId)
      }
      else {
        BrowserHyperlinkListener.INSTANCE.hyperlinkUpdate(e)
      }
    }
  }

  init {
    border = JBUI.Borders.empty(CommitDetailsPanel.EXTERNAL_BORDER, CommitDetailsPanel.SIDE_BORDER, CommitDetailsPanel.INTERNAL_BORDER, 0)
  }

  fun updateMessage(message: CommitPresentation?) {
    presentation = message
    update()
  }

  override fun getBody() = presentation?.text ?: ""

  override fun getBackground(): Color = getCommitDetailsBackground()

  override fun update() {
    isVisible = presentation != null
    super.update()
  }
}

private class ContainingBranchesPanel : HtmlPanel() {
  private var branches: List<String>? = null
  private var expanded = false

  init {
    border = JBUI.Borders.empty(0, CommitDetailsPanel.SIDE_BORDER, CommitDetailsPanel.EXTERNAL_BORDER, 0)
    isVisible = false
  }

  override fun setBounds(x: Int, y: Int, w: Int, h: Int) {
    val oldWidth = width
    super.setBounds(x, y, w, h)
    if (w != oldWidth) {
      update()
    }
  }

  override fun hyperlinkUpdate(e: HyperlinkEvent) {
    if (e.eventType == HyperlinkEvent.EventType.ACTIVATED && isShowHideBranches(e)) {
      expanded = !expanded
      update()
    }
  }

  fun setBranches(branches: List<String>?) {
    this.branches = branches
    expanded = false
    isVisible = true

    update()
  }

  override fun getBody(): String {
    val insets = insets
    val text = getBranchesText(branches, expanded, width - insets.left - insets.right, getFontMetrics(bodyFont))
    return if (expanded) text else HtmlChunk.raw(text).wrapWith("nobr").toString()
  }

  override fun getBackground(): Color = getCommitDetailsBackground()

  override fun getBodyFont(): Font = FontUtil.getCommitMetadataFont()
}

private class HashAndAuthorPanel : HtmlPanel() {

  init {
    editorKit = HTMLEditorKitBuilder()
      .withViewFactoryExtensions(ExtendableHTMLViewFactory.Extensions.WORD_WRAP,
                                 ExtendableHTMLViewFactory.Extensions.icons {
                                   signature?.icon
                                 }
      )
      .build().apply {
        //language=css
        styleSheet.addRule(""".signature {
            color: ${ColorUtil.toHtmlColor(UIUtil.getContextHelpForeground())};
        }""".trimMargin())
      }
  }

  var presentation: CommitPresentation? = null
    set(value) {
      field = value
      update()
    }

  var signature: VcsCommitExternalStatusPresentation.Signature? = null
    set(value) {
      field = value
      update()
    }

  override fun getBody(): String {
    val presentation = presentation ?: return ""
    val signature = signature

    @Suppress("HardCodedStringLiteral")
    return presentation.hashAndAuthor.let {
      if (signature != null) {
        val tooltip = signature.description?.toString()
        //language=html
        it + """<span class='signature'>&nbsp;&nbsp;&nbsp; 
          |<icon src='sig' alt='${tooltip.orEmpty()}'/>
          |&nbsp;${signature.text}
          |</span>""".trimMargin()
      }
      else it
    }
  }

  init {
    border = JBUI.Borders.empty()
  }

  public override fun getBodyFont(): Font = FontUtil.getCommitMetadataFont()

  override fun update() {
    isVisible = presentation != null
    super.update()
  }
}

private class RootColorPanel(private val parent: HashAndAuthorPanel) : Wrapper(parent) {
  companion object {
    private const val ROOT_ICON_SIZE = 13
    private const val ROOT_GAP = 4
  }

  private var icon: ColorIcon? = null
  private var tooltipText: String? = null
  private val mouseMotionListener = object : MouseAdapter() {
    override fun mouseMoved(e: MouseEvent?) {
      if (IdeTooltipManager.getInstance().hasCurrent()) {
        IdeTooltipManager.getInstance().hideCurrent(e)
        return
      }
      icon?.let { icon ->
        tooltipText?.let { tooltipText ->
          VcsLogUiUtil.showTooltip(this@RootColorPanel, Point(icon.iconWidth / 2, 0), Balloon.Position.above, tooltipText)
        }
      }
    }
  }

  init {
    setVerticalSizeReferent(parent)
    addMouseMotionListener(mouseMotionListener)
  }

  override fun getPreferredSize(): Dimension = icon?.let { icon ->
    val size = super.getPreferredSize()
    Dimension(icon.iconWidth + JBUIScale.scale(ROOT_GAP), size.height)
  } ?: Dimension(0, 0)

  fun setRoot(rootColor: CommitDetailsPanel.RootColor?) {
    if (rootColor != null) {
      icon = JBUIScale.scaleIcon(ColorIcon(ROOT_ICON_SIZE, rootColor.color))
      tooltipText = rootColor.root.path
    }
    else {
      icon = null
      tooltipText = null
    }
  }

  fun update() {
    isVisible = icon != null
    revalidate()
    repaint()
  }

  override fun getBackground(): Color = getCommitDetailsBackground()

  override fun paintComponent(g: Graphics) {
    icon?.let { icon ->
      val h = FontUtil.getStandardAscent(parent.bodyFont, g)
      val metrics = getFontMetrics(parent.bodyFont)
      icon.paintIcon(this, g, 0, metrics.maxAscent - h + (h - icon.iconHeight - 1) / 2)
    }
  }
}

fun getCommitDetailsBackground(): Color = UIUtil.getTreeBackground()