vcs: clone dialog API cleanup & refactoring

* fix icon/labels scaling in items list
* extract widely used constants in separate "Spec" object
* add ability to render additional lines in component items list
* add ability to notify Clone dialog about internal component state changes
* add some javadoc and @CalledOnAwt annotations

GitOrigin-RevId: cba1f784ce705304d3339189c9a19ac9ac0e84c9
This commit is contained in:
Aleksandr Krasilnikov
2019-07-23 17:44:15 +03:00
committed by intellij-monorepo-bot
parent d625488698
commit fa1f5b9c73
11 changed files with 213 additions and 82 deletions
@@ -24,4 +24,6 @@ class VcsCloneComponentStub(private val checkoutProvider: CheckoutProvider,
override fun isOkEnabled() = true
override fun doValidateAll() = emptyList<ValidationInfo>()
override fun getOkButtonText() = primaryActionText
}
@@ -0,0 +1,15 @@
// 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.openapi.vcs.ui.cloneDialog
import java.util.*
/**
* Informs the parent dialog about need to update its state regarding to the state of internal component
*/
interface VcsCloneDialogComponentStateListener : EventListener {
fun onOkActionNameChanged(name: String)
fun onOkActionEnabled(enabled: Boolean)
fun onListItemChanged()
}
@@ -24,6 +24,16 @@ interface VcsCloneDialogExtension {
*/
fun getIcon(): Icon
/**
* Additional status lines, which may contain some info and actions related to authorized accounts, internal errors, etc
*/
fun getAdditionalStatusLines(): List<VcsCloneDialogExtensionStatusLine> = emptyList()
/**
* Optional tooltip for extension item
*/
fun getTooltip(): String? = null
/**
* Builds [VcsCloneDialogExtensionComponent] that would be displayed on center of get-from-vcs dialog when extension is selected.
* Will be called lazily and once on first choosing of extension.
@@ -1,7 +1,10 @@
// 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.openapi.vcs.ui.cloneDialog
import com.intellij.openapi.Disposable
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.util.EventDispatcher
import org.jetbrains.annotations.CalledInAwt
import javax.swing.JComponent
/**
@@ -10,36 +13,51 @@ import javax.swing.JComponent
* 1. Providing extension-specific UI for get-from-vcs dialog by [getView]
* 2. handling commands that would be provided from top-level [com.intellij.openapi.ui.DialogWrapper]
*/
interface VcsCloneDialogExtensionComponent {
abstract class VcsCloneDialogExtensionComponent : Disposable {
private val listeners = EventDispatcher.create(VcsCloneDialogComponentStateListener::class.java)
protected val dialogStateListener = listeners.multicaster
/**
* Return main [JComponent] that will be displayed in center of get-from-vcs dialog when extension is selected.
*
* It is called once when extension is selected first time
*/
fun getView(): JComponent
@CalledInAwt
abstract fun getView(): JComponent
/**
* Checks that is possible to perform ok action in current state of component
*
* Would be called form [com.intellij.openapi.ui.DialogWrapper.isOKActionEnabled] when extension is selected
* Performs primary clone/checkout action. [doClone] is called from the UI-thread, but internal heavy clone task should be scheduled
* in background
*/
fun isOkEnabled(): Boolean
/**
* Perform primary clone/checkout action.
*/
fun doClone()
@CalledInAwt
abstract fun doClone()
/**
* would be called from [com.intellij.openapi.ui.DialogWrapper.doValidateAll] when extension is selected
*
* @see com.intellij.openapi.ui.DialogWrapper.doValidateAll
*/
fun doValidateAll(): List<ValidationInfo>
@CalledInAwt
abstract fun doValidateAll(): List<ValidationInfo>
/**
* Text that represents dialog primary action
* Adds listener that would be called from component in order to notify parent dialog about internal state
*
* @see VcsCloneDialogComponentStateListener
*/
fun getOkButtonText(): String = "Clone"
@CalledInAwt
fun addComponentStateListener(componentStateListener: VcsCloneDialogComponentStateListener) {
listeners.addListener(componentStateListener)
}
@CalledInAwt
fun removeComponentListener(componentStateListener: VcsCloneDialogComponentStateListener) {
listeners.removeListener(componentStateListener)
}
@CalledInAwt
abstract fun onComponentSelected()
final override fun dispose() = listeners.listeners.clear()
}
@@ -0,0 +1,16 @@
// 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.openapi.vcs.ui.cloneDialog
import com.intellij.ui.SimpleTextAttributes
import java.awt.event.ActionListener
data class VcsCloneDialogExtensionStatusLine(val text: String,
val attribute: SimpleTextAttributes,
val actionListener: ActionListener? = null) {
companion object {
fun greyText(text: String): VcsCloneDialogExtensionStatusLine {
return VcsCloneDialogExtensionStatusLine(text, SimpleTextAttributes.GRAY_ATTRIBUTES)
}
}
}
@@ -12,8 +12,7 @@ import com.intellij.openapi.vcs.ui.cloneDialog.VcsCloneDialogExtension
import com.intellij.openapi.vcs.ui.cloneDialog.VcsCloneDialogExtensionComponent
import com.intellij.ui.SimpleListCellRenderer
import com.intellij.ui.layout.*
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.intellij.util.ui.JBEmptyBorder
import java.awt.BorderLayout
import java.awt.CardLayout
import java.awt.event.ItemEvent
@@ -22,15 +21,28 @@ import javax.swing.Icon
import javax.swing.JPanel
class RepositoryUrlCloneDialogExtension : VcsCloneDialogExtension {
private val tooltip = CheckoutProvider.EXTENSION_POINT_NAME.extensions
.map { it.vcsName }
.joinToString { it.replace("_".toRegex(), "") }
override fun getIcon(): Icon = AllIcons.Welcome.FromVCS
override fun getName() = "Repository URL"
override fun getTooltip(): String? {
return tooltip
}
override fun createMainComponent(project: Project): RepositoryUrlMainExtensionComponent {
return RepositoryUrlMainExtensionComponent(project)
}
class RepositoryUrlMainExtensionComponent(private val project: Project) : VcsCloneDialogExtensionComponent {
class RepositoryUrlMainExtensionComponent(private val project: Project) : VcsCloneDialogExtensionComponent() {
override fun onComponentSelected() {
dialogStateListener.onOkActionNameChanged(getCurrentVcsComponent()?.getOkButtonText() ?: "Clone")
dialogStateListener.onOkActionEnabled(true)
}
private val vcsComponents = HashMap<String, VcsCloneComponent>()
private val cardLayout = CardLayout()
private val mainPanel = JPanel(BorderLayout())
@@ -39,7 +51,7 @@ class RepositoryUrlCloneDialogExtension : VcsCloneDialogExtension {
}
init {
mainPanel.border = JBUI.Borders.emptyRight(UIUtil.PANEL_REGULAR_INSETS.right)
mainPanel.border = JBEmptyBorder(VcsCloneDialogUiSpec.Components.rightInsets)
val northPanel = panel {
row("Version control:") { comboBox() }
}
@@ -51,6 +63,7 @@ class RepositoryUrlCloneDialogExtension : VcsCloneDialogExtension {
comboBox.addItemListener { e: ItemEvent ->
if (e.stateChange == ItemEvent.SELECTED) {
val provider = e.item as CheckoutProvider
onComponentSelected()
cardLayout.show(centerPanel, provider.vcsName)
}
}
@@ -66,10 +79,6 @@ class RepositoryUrlCloneDialogExtension : VcsCloneDialogExtension {
override fun getView() = mainPanel
override fun getOkButtonText(): String {
return getCurrentVcsComponent()?.getOkButtonText() ?: "Clone"
}
fun openForVcs(clazz: Class<out CheckoutProvider>): RepositoryUrlMainExtensionComponent {
comboBox.selectedItem = CheckoutProvider.EXTENSION_POINT_NAME.findExtension(clazz)
return this
@@ -80,10 +89,6 @@ class RepositoryUrlCloneDialogExtension : VcsCloneDialogExtension {
getCurrentVcsComponent()?.doClone(project, listener)
}
override fun isOkEnabled(): Boolean {
return getCurrentVcsComponent()?.isOkEnabled() ?: true
}
override fun doValidateAll(): List<ValidationInfo> {
return getCurrentVcsComponent()?.doValidateAll() ?: emptyList()
}
@@ -2,21 +2,17 @@
package com.intellij.util.ui.cloneDialog
import com.intellij.openapi.project.Project
import com.intellij.openapi.rd.attachChild
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.vcs.CheckoutProvider
import com.intellij.openapi.vcs.ui.cloneDialog.VcsCloneDialogComponentStateListener
import com.intellij.openapi.vcs.ui.cloneDialog.VcsCloneDialogExtension
import com.intellij.openapi.vcs.ui.cloneDialog.VcsCloneDialogExtensionComponent
import com.intellij.openapi.wm.impl.welcomeScreen.FlatWelcomeFrame
import com.intellij.openapi.wm.impl.welcomeScreen.FlatWelcomeFrame.getSeparatorColor
import com.intellij.ui.CollectionListModel
import com.intellij.ui.ScrollPaneFactory
import com.intellij.ui.ScrollingUtil
import com.intellij.ui.border.CustomLineBorder
import com.intellij.ui.*
import com.intellij.util.ui.JBEmptyBorder
import com.intellij.util.ui.JBInsets
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.intellij.util.ui.cloneDialog.RepositoryUrlCloneDialogExtension.RepositoryUrlMainExtensionComponent
import java.awt.CardLayout
import java.util.*
@@ -35,6 +31,20 @@ internal class VcsCloneDialog private constructor(private val project: Project,
private val cardLayout = CardLayout()
private val mainPanel = JPanel(cardLayout)
private val extensionComponents: MutableMap<String, VcsCloneDialogExtensionComponent> = HashMap()
private val listModel = CollectionListModel<VcsCloneDialogExtension>(VcsCloneDialogExtension.EP_NAME.extensionList)
private val listener = object : VcsCloneDialogComponentStateListener {
override fun onOkActionNameChanged(name: String) = setOKButtonText(name)
override fun onOkActionEnabled(enabled: Boolean) {
isOKActionEnabled = enabled
}
override fun onListItemChanged() {
listModel.allContentsChanged()
pack()
}
}
init {
init()
@@ -44,11 +54,7 @@ internal class VcsCloneDialog private constructor(private val project: Project,
rootPane.preferredSize = it
}
val withoutRightInset = UIUtil.PANEL_REGULAR_INSETS.let {
// use empty right inset to align the scroll bar to the edge of panel
JBInsets(it.top, it.left, it.bottom, 0)
}
mainPanel.border = JBEmptyBorder(withoutRightInset)
mainPanel.border = JBEmptyBorder(VcsCloneDialogUiSpec.Dialog.mainComponentParentInsets)
VcsCloneDialogExtension.EP_NAME.findExtension(initialExtensionClass)?.let {
ScrollingUtil.selectItem(extensionList, it)
@@ -58,9 +64,6 @@ internal class VcsCloneDialog private constructor(private val project: Project,
override fun getStyle() = DialogStyle.COMPACT
override fun createCenterPanel(): JComponent {
val extensions = VcsCloneDialogExtension.EP_NAME.extensionList
val listModel = CollectionListModel<VcsCloneDialogExtension>(extensions)
extensionList = VcsCloneDialogExtensionList(listModel).apply {
addListSelectionListener(ListSelectionListener { e ->
val source = e.source as VcsCloneDialogExtensionList
@@ -68,19 +71,14 @@ internal class VcsCloneDialog private constructor(private val project: Project,
})
}
val scrollableList = ScrollPaneFactory.createScrollPane(extensionList, true).apply {
border = CustomLineBorder(getSeparatorColor(), JBUI.insetsRight(1))
border = IdeBorderFactory.createBorder(SideBorder.RIGHT)
}
return JBUI.Panels.simplePanel()
.addToCenter(mainPanel)
.addToLeft(scrollableList).apply {
border = CustomLineBorder(getSeparatorColor(), JBUI.insetsBottom(1))
}
.addToLeft(scrollableList)
}
override fun isOKActionEnabled(): Boolean {
return getSelectedComponent()?.isOkEnabled() ?: false
}
override fun doValidateAll(): List<ValidationInfo> {
return getSelectedComponent()?.doValidateAll() ?: emptyList()
}
@@ -97,13 +95,15 @@ internal class VcsCloneDialog private constructor(private val project: Project,
scrollableMainPanel.verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED
scrollableMainPanel.horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
mainPanel.add(scrollableMainPanel, extensionId)
disposable.attachChild(component)
component.addComponentStateListener(listener)
component
})
setOKButtonText(mainComponent.getOkButtonText())
if (mainComponent is RepositoryUrlMainExtensionComponent) {
initialVcs?.let { mainComponent.openForVcs(it) }
}
mainComponent.onComponentSelected()
cardLayout.show(mainPanel, extensionId)
}
@@ -8,39 +8,35 @@ import com.intellij.ui.components.JBList
import com.intellij.util.ui.ListUiUtil
import com.intellij.util.ui.UIUtil
import java.awt.Component
import java.awt.FlowLayout
import javax.swing.*
import javax.swing.border.EmptyBorder
import javax.swing.JList
import javax.swing.ListCellRenderer
import javax.swing.ListModel
class VcsCloneDialogExtensionList(listModel: ListModel<VcsCloneDialogExtension>) : JBList<VcsCloneDialogExtension>(listModel) {
init {
selectionModel = SingleSelectionModel()
cellRenderer = Renderer()
val renderer = Renderer()
cellRenderer = renderer
UIUtil.putClientProperty(this, UIUtil.NOT_IN_HIERARCHY_COMPONENTS, listOf(renderer))
ScrollingUtil.installActions(this)
}
class Renderer : ListCellRenderer<VcsCloneDialogExtension> {
val component = VcsCloneDialogExtensionListItem
val wrapper: JComponent
init {
wrapper = JPanel(FlowLayout(FlowLayout.LEADING)).apply {
border = EmptyBorder(UIUtil.PANEL_REGULAR_INSETS)
add(component)
}
}
class Renderer
: ListCellRenderer<VcsCloneDialogExtension>,
VcsCloneDialogExtensionListItem() {
override fun getListCellRendererComponent(list: JList<out VcsCloneDialogExtension>,
value: VcsCloneDialogExtension,
extension: VcsCloneDialogExtension,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean): Component {
component.setTitle(value.getName())
component.setTitleForeground(ListUiUtil.WithTallRow.foreground(list, isSelected))
component.setIcon(value.getIcon())
UIUtil.setBackgroundRecursively(wrapper, ListUiUtil.WithTallRow.background(list, isSelected))
return wrapper
setTitle(extension.getName())
setTitleForeground(ListUiUtil.WithTallRow.foreground(list, isSelected))
setIcon(extension.getIcon())
toolTipText = extension.getTooltip()
setAdditionalStatusLines(extension.getAdditionalStatusLines())
UIUtil.setBackgroundRecursively(this, ListUiUtil.WithTallRow.background(list, isSelected))
return this
}
}
}
@@ -1,37 +1,51 @@
// 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.util.ui.cloneDialog
import com.intellij.openapi.vcs.ui.cloneDialog.VcsCloneDialogExtensionStatusLine
import com.intellij.ui.SimpleColoredComponent
import com.intellij.ui.components.panels.VerticalLayout
import com.intellij.util.IconUtil
import com.intellij.util.ui.GridBag
import com.intellij.util.ui.JBEmptyBorder
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import java.awt.Color
import java.awt.GridBagLayout
import javax.swing.Icon
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.SwingConstants
object VcsCloneDialogExtensionListItem : JComponent() {
open class VcsCloneDialogExtensionListItem : JPanel(GridBagLayout()) {
private val iconLabel: JLabel = JLabel()
private val titleLabel: JLabel = JLabel()
init {
layout = GridBagLayout()
private val labelsPool = ArrayList<SimpleColoredComponent>()
private val additionalLinesPanel = JPanel(VerticalLayout(0, SwingConstants.LEFT))
var gbc = GridBag().next()
.insetRight(UIUtil.DEFAULT_VGAP)
init {
border = JBEmptyBorder(VcsCloneDialogUiSpec.ExtensionsList.insets)
relayout()
}
private fun relayout() {
var gbc = GridBag().nextLine().next()
.insets(JBUI.insetsRight(VcsCloneDialogUiSpec.ExtensionsList.iconTitleGap))
.weightx(0.0)
.anchor(GridBag.LINE_START)
.fillCellNone()
add(iconLabel, gbc)
gbc = gbc.next()
.weightx(1.0)
.insets(JBUI.emptyInsets())
.anchor(GridBag.BELOW_BASELINE_LEADING)
.fillCellHorizontally()
titleLabel.font = JBUI.Fonts.label().asBold()
add(titleLabel, gbc)
gbc = gbc.nextLine().next().next()
.insets(JBUI.emptyInsets())
.fillCellHorizontally()
add(additionalLinesPanel, gbc)
}
fun setTitle(title: String) {
@@ -39,7 +53,24 @@ object VcsCloneDialogExtensionListItem : JComponent() {
}
fun setIcon(icon: Icon) {
iconLabel.icon = IconUtil.scale(icon, null, 22.0f/icon.iconHeight) // scale is chosen so that the size of icon corresponds the design
val scale = VcsCloneDialogUiSpec.ExtensionsList.iconSize.float / icon.iconWidth
iconLabel.icon = IconUtil.scale(icon, null, scale)
}
fun setAdditionalStatusLines(additionalLines: List<VcsCloneDialogExtensionStatusLine>) {
additionalLinesPanel.removeAll()
while (labelsPool.size < additionalLines.size) {
labelsPool.add(SimpleColoredComponent())
}
for ((index, line) in additionalLines.withIndex()) {
val component = labelsPool[index]
component.ipad = JBUI.insets(0, 0)
component.clear()
// TODO: add handling for long text lines
component.append(line.text, line.attribute, line.actionListener)
additionalLinesPanel.add(component)
}
}
fun setTitleForeground(foreground: Color) {
@@ -0,0 +1,30 @@
// 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.util.ui.cloneDialog
import com.intellij.util.ui.JBInsets
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.JBValue
import com.intellij.util.ui.UIUtil
/**
* Contains a lot of UI specific constants for clone dialog that can be helpful for external implementations.
*/
object VcsCloneDialogUiSpec {
object Dialog {
val mainComponentParentInsets = UIUtil.PANEL_REGULAR_INSETS.let {
// use empty right inset to align the scroll bar to the edge of panel
JBInsets(it.top, it.left, it.bottom, 0)
}
}
object ExtensionsList {
val iconSize = JBValue.UIInteger("VcsCloneDialog.iconSize", 22)
const val iconTitleGap = 6
val insets = JBUI.insets(8, 10)
}
object Components {
// insets for component without full-height scrollbar
val rightInsets = JBUI.insetsRight((UIUtil.PANEL_REGULAR_INSETS.right))
}
}
@@ -16,6 +16,8 @@ import com.intellij.openapi.vcs.CheckoutProvider;
import com.intellij.openapi.vcs.VcsConfiguration;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.ex.ProjectLevelVcsManagerEx;
import com.intellij.openapi.vcs.ui.VcsCloneComponent;
import com.intellij.openapi.vcs.ui.VcsCloneComponentStub;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.StatusBar;
@@ -339,4 +341,10 @@ public class SvnCheckoutProvider implements CheckoutProvider {
return result;
}
}
@NotNull
@Override
public VcsCloneComponent buildVcsCloneComponent(@NotNull Project project) {
return new VcsCloneComponentStub(this, "Checkout");
}
}