[github] Fix accounts list relogin link

Materialise a renderer over a hovered list row instead of complicated mouse listeners

Fixes IDEA-233345

GitOrigin-RevId: 992dbdf905046791c90109c3affbf196b69240a8
This commit is contained in:
Ivan Semenov
2020-05-08 10:59:57 +00:00
committed by intellij-monorepo-bot
parent d6440d2d13
commit c886fb8582
2 changed files with 181 additions and 169 deletions
@@ -12,12 +12,11 @@ import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.ui.CollectionListModel
import com.intellij.ui.SimpleColoredComponent
import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.SimpleTextAttributes.STYLE_PLAIN
import com.intellij.ui.SimpleTextAttributes.STYLE_UNDERLINE
import com.intellij.ui.ToolbarDecorator
import com.intellij.ui.components.JBList
import com.intellij.ui.components.labels.LinkLabel
import com.intellij.ui.components.labels.LinkListener
import com.intellij.util.progress.ProgressVisibilityManager
import com.intellij.util.ui.*
import com.intellij.util.ui.components.BorderLayoutPanel
@@ -31,47 +30,34 @@ import org.jetbrains.plugins.github.exceptions.GithubAuthenticationException
import org.jetbrains.plugins.github.i18n.GithubBundle
import org.jetbrains.plugins.github.pullrequest.avatars.CachingGithubAvatarIconsProvider
import org.jetbrains.plugins.github.pullrequest.avatars.GHAvatarIconsProvider
import org.jetbrains.plugins.github.ui.util.JListHoveredRowMaterialiser
import org.jetbrains.plugins.github.util.CachingGithubUserAvatarLoader
import org.jetbrains.plugins.github.util.GithubImageResizer
import org.jetbrains.plugins.github.util.GithubUIUtil
import java.awt.*
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.*
import javax.swing.event.ListDataEvent
import javax.swing.event.ListDataListener
private const val LINK_TAG = "EDIT_LINK"
internal class GHAccountsPanel(private val project: Project,
private val executorFactory: GithubApiRequestExecutor.Factory,
private val avatarLoader: CachingGithubUserAvatarLoader,
private val imageResizer: GithubImageResizer) : BorderLayoutPanel(), Disposable {
private val accountListModel = CollectionListModel<GithubAccountDecorator>().apply {
// disable link handler when there are no errors
addListDataListener(object : ListDataListener {
override fun contentsChanged(e: ListDataEvent?) = setLinkHandlerEnabled(items.any { it.errorText != null })
override fun intervalRemoved(e: ListDataEvent?) {}
override fun intervalAdded(e: ListDataEvent?) {}
})
}
private val accountListModel = CollectionListModel<GithubAccountDecorator>()
private val accountList = JBList<GithubAccountDecorator>(accountListModel).apply {
val decoratorRenderer = GithubAccountDecoratorRenderer()
cellRenderer = decoratorRenderer
JListHoveredRowMaterialiser.install(this, GithubAccountDecoratorRenderer())
UIUtil.putClientProperty(this, UIUtil.NOT_IN_HIERARCHY_COMPONENTS, listOf(decoratorRenderer))
selectionMode = ListSelectionModel.SINGLE_SELECTION
emptyText.apply {
appendText(GithubBundle.message("accounts.none.added"))
appendSecondaryText(GithubBundle.message("accounts.add"), SimpleTextAttributes.LINK_PLAIN_ATTRIBUTES, { addAccount() })
appendSecondaryText(GithubBundle.message("accounts.add"), SimpleTextAttributes.LINK_PLAIN_ATTRIBUTES) { addAccount() }
appendSecondaryText(" (${KeymapUtil.getFirstKeyboardShortcutText(CommonShortcuts.getNew())})", StatusText.DEFAULT_ATTRIBUTES, null)
}
}
private val progressManager = createListProgressManager()
private val errorLinkHandler = createLinkActivationListener()
private var errorLinkHandlerInstalled = false
private var currentTokensMap = mapOf<GithubAccount, String?>()
private val newTokensMap = mutableMapOf<GithubAccount, String>()
@@ -131,82 +117,6 @@ internal class GHAccountsPanel(private val project: Project,
private fun isAccountUnique(login: String, server: GithubServerPath) =
accountListModel.items.none { it.account.name == login && it.account.server == server }
/**
* Manages link hover and click for [GithubAccountDecoratorRenderer.loadingError]
* Sets the proper cursor and underlines the link on hover
*
* @see [GithubAccountDecorator.errorText]
* @see [GithubAccountDecorator.showReLoginLink]
* @see [GithubAccountDecorator.errorLinkPointedAt]
*/
private fun createLinkActivationListener() = object : MouseAdapter() {
override fun mouseMoved(e: MouseEvent) {
val decorator = findDecoratorWithLoginLinkAt(e.point)
if (decorator != null) {
UIUtil.setCursor(accountList, Cursor.getPredefinedCursor(Cursor.HAND_CURSOR))
}
else {
UIUtil.setCursor(accountList, Cursor.getDefaultCursor())
}
var hasChanges = false
for (item in accountListModel.items) {
val isLinkPointedAt = item == decorator
hasChanges = hasChanges || isLinkPointedAt != item.errorLinkPointedAt
item.errorLinkPointedAt = isLinkPointedAt
}
if (hasChanges) accountListModel.allContentsChanged()
}
override fun mouseClicked(e: MouseEvent) {
findDecoratorWithLoginLinkAt(e.point)?.run(::editAccount)
}
/**
* Checks if mouse is pointed at decorator error link
*
* @return decorator with error link under mouse pointer or null
*/
private fun findDecoratorWithLoginLinkAt(point: Point): GithubAccountDecorator? {
val idx = accountList.locationToIndex(point)
if (idx < 0) return null
val cellBounds = accountList.getCellBounds(idx, idx)
if (!cellBounds.contains(point)) return null
val decorator = accountListModel.getElementAt(idx)
if (decorator?.errorText == null) return null
val rendererComponent = accountList.cellRenderer.getListCellRendererComponent(accountList, decorator, idx, true, true)
rendererComponent.setBounds(cellBounds.x, cellBounds.y, cellBounds.width, cellBounds.height)
UIUtil.layoutRecursively(rendererComponent)
val rendererRelativeX = point.x - cellBounds.x
val rendererRelativeY = point.y - cellBounds.y
val childComponent = UIUtil.getDeepestComponentAt(rendererComponent, rendererRelativeX, rendererRelativeY)
if (childComponent !is SimpleColoredComponent) return null
val childRelativeX = rendererRelativeX - childComponent.parent.x - childComponent.x
return if (childComponent.getFragmentTagAt(childRelativeX) == LINK_TAG) decorator else null
}
}
private fun setLinkHandlerEnabled(enabled: Boolean) {
if (enabled) {
if (!errorLinkHandlerInstalled) {
accountList.addMouseListener(errorLinkHandler)
accountList.addMouseMotionListener(errorLinkHandler)
errorLinkHandlerInstalled = true
}
}
else if (errorLinkHandlerInstalled) {
accountList.removeMouseListener(errorLinkHandler)
accountList.removeMouseMotionListener(errorLinkHandler)
errorLinkHandlerInstalled = false
}
}
fun loadExistingAccountsDetails() {
for (accountData in accountListModel.items) {
loadAccountDetails(accountData)
@@ -286,87 +196,85 @@ internal class GHAccountsPanel(private val project: Project,
}
override fun dispose() {}
}
private class GithubAccountDecoratorRenderer : ListCellRenderer<GithubAccountDecorator>, JPanel() {
private val accountName = JLabel()
private inner class GithubAccountDecoratorRenderer : ListCellRenderer<GithubAccountDecorator>, JPanel() {
private val accountName = JLabel()
private val serverName = JLabel()
private val profilePicture = JLabel()
private val serverName = JLabel()
private val profilePicture = JLabel()
private val fullName = JLabel()
private val fullName = JLabel()
private val loadingError = SimpleColoredComponent()
private val loadingError = JLabel()
private val reloginLink = LinkLabel<Any?>(GithubBundle.message("accounts.relogin"), null)
/**
* UPDATE [createLinkActivationListener] IF YOU CHANGE LAYOUT
*/
init {
layout = FlowLayout(FlowLayout.LEFT, 0, 0)
border = JBUI.Borders.empty(5, 8)
/**
* UPDATE [createLinkActivationListener] IF YOU CHANGE LAYOUT
*/
init {
layout = FlowLayout(FlowLayout.LEFT, 0, 0)
border = JBUI.Borders.empty(5, 8)
val namesPanel = JPanel().apply {
layout = GridBagLayout()
border = JBUI.Borders.empty(0, 6, 4, 6)
val namesPanel = JPanel().apply {
layout = GridBagLayout()
border = JBUI.Borders.empty(0, 6, 4, 6)
val bag = GridBag()
.setDefaultInsets(JBUI.insetsRight(UIUtil.DEFAULT_HGAP))
.setDefaultAnchor(GridBagConstraints.WEST)
.setDefaultFill(GridBagConstraints.VERTICAL)
add(fullName, bag.nextLine().next())
add(accountName, bag.next())
add(loadingError, bag.next())
add(serverName, bag.nextLine().coverLine())
}
add(profilePicture)
add(namesPanel)
}
override fun getListCellRendererComponent(list: JList<out GithubAccountDecorator>,
value: GithubAccountDecorator,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean): Component {
UIUtil.setBackgroundRecursively(this, ListUiUtil.WithTallRow.background(list, isSelected, list.hasFocus()))
val primaryTextColor = ListUiUtil.WithTallRow.foreground(isSelected, list.hasFocus())
val secondaryTextColor = ListUiUtil.WithTallRow.secondaryForeground(list, isSelected)
accountName.apply {
text = value.account.name
setBold(if (value.details?.name == null) value.projectDefault else false)
foreground = if (value.details?.name == null) primaryTextColor else secondaryTextColor
}
serverName.apply {
text = value.account.server.toString()
foreground = secondaryTextColor
}
profilePicture.apply {
icon = value.getIcon()
}
fullName.apply {
text = value.details?.name
setBold(value.projectDefault)
isVisible = value.details?.name != null
foreground = primaryTextColor
}
loadingError.apply {
clear()
value.errorText?.let {
append(it, SimpleTextAttributes.ERROR_ATTRIBUTES)
append(" ")
if (value.showReLoginLink) append(GithubBundle.message("accounts.relogin"),
if (value.errorLinkPointedAt)
SimpleTextAttributes(STYLE_UNDERLINE, JBUI.CurrentTheme.Link.linkColor())
else
SimpleTextAttributes(STYLE_PLAIN, JBUI.CurrentTheme.Link.linkColor()),
LINK_TAG)
val bag = GridBag()
.setDefaultInsets(JBUI.insetsRight(UIUtil.DEFAULT_HGAP))
.setDefaultAnchor(GridBagConstraints.WEST)
.setDefaultFill(GridBagConstraints.VERTICAL)
add(fullName, bag.nextLine().next())
add(accountName, bag.next())
add(loadingError, bag.next())
add(reloginLink, bag.next())
add(serverName, bag.nextLine().coverLine())
}
}
return this
}
companion object {
add(profilePicture)
add(namesPanel)
}
override fun getListCellRendererComponent(list: JList<out GithubAccountDecorator>,
value: GithubAccountDecorator,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean): Component {
UIUtil.setBackgroundRecursively(this, ListUiUtil.WithTallRow.background(list, isSelected, list.hasFocus()))
val primaryTextColor = ListUiUtil.WithTallRow.foreground(isSelected, list.hasFocus())
val secondaryTextColor = ListUiUtil.WithTallRow.secondaryForeground(list, isSelected)
accountName.apply {
text = value.account.name
setBold(if (value.details?.name == null) value.projectDefault else false)
foreground = if (value.details?.name == null) primaryTextColor else secondaryTextColor
}
serverName.apply {
text = value.account.server.toString()
foreground = secondaryTextColor
}
profilePicture.apply {
icon = value.getIcon()
}
fullName.apply {
text = value.details?.name
setBold(value.projectDefault)
isVisible = value.details?.name != null
foreground = primaryTextColor
}
loadingError.apply {
text = value.errorText
foreground = UIUtil.getErrorForeground()
}
reloginLink.apply {
isVisible = value.errorText != null && value.showReLoginLink
setListener(LinkListener { _, _ ->
editAccount(value)
}, null)
}
return this
}
private fun JLabel.setBold(isBold: Boolean) {
font = font.deriveFont(if (isBold) font.style or Font.BOLD else font.style and Font.BOLD.inv())
}
@@ -382,7 +290,6 @@ private class GithubAccountDecorator(val account: GithubAccount, var projectDefa
var errorText: String? = null
var showReLoginLink = false
var errorLinkPointedAt = false
override fun equals(other: Any?): Boolean {
if (this === other) return true
@@ -0,0 +1,105 @@
// 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.
package org.jetbrains.plugins.github.ui.util
import com.intellij.ui.ExpandedItemListCellRendererWrapper
import java.awt.Component
import java.awt.event.*
import javax.swing.JList
import javax.swing.ListCellRenderer
import javax.swing.event.ListDataEvent
import javax.swing.event.ListDataListener
import javax.swing.event.ListSelectionEvent
import javax.swing.event.ListSelectionListener
import kotlin.properties.Delegates
/**
* Materializes a concrete component in a hovered list cell that matches the ones painted by the renderer
*/
class JListHoveredRowMaterialiser<T> private constructor(private val list: JList<T>,
private val cellRenderer: ListCellRenderer<T>) {
private var hoveredIndex: Int by Delegates.observable(-1) { _, oldValue, newValue ->
if (newValue != oldValue)
materialiseRendererAt(newValue)
}
private var rendererComponent: Component? = null
private val listRowHoverListener = object : MouseMotionAdapter() {
override fun mouseMoved(e: MouseEvent) {
val point = e.point
val idx = list.locationToIndex(point)
if (idx >= 0 && list.getCellBounds(idx, idx).contains(point)) {
hoveredIndex = idx
}
else {
hoveredIndex = -1
}
}
}
private val listDataListener = object : ListDataListener {
override fun contentsChanged(e: ListDataEvent) {
if (hoveredIndex in e.index0..e.index1) materialiseRendererAt(hoveredIndex)
}
override fun intervalRemoved(e: ListDataEvent) {
if (hoveredIndex > e.index0 || hoveredIndex > e.index1) hoveredIndex = -1
}
override fun intervalAdded(e: ListDataEvent) {
if (hoveredIndex > e.index0 || hoveredIndex > e.index1) hoveredIndex = -1
}
}
private val listPresentationListener = object : FocusListener,
ListSelectionListener,
ComponentAdapter() {
override fun focusLost(e: FocusEvent) = materialiseRendererAt(hoveredIndex)
override fun focusGained(e: FocusEvent) = materialiseRendererAt(hoveredIndex)
override fun valueChanged(e: ListSelectionEvent) = materialiseRendererAt(hoveredIndex)
override fun componentResized(e: ComponentEvent) = materialiseRendererAt(hoveredIndex)
}
private fun materialiseRendererAt(index: Int) {
if (index < 0 || index > list.model.size - 1) {
rendererComponent?.let { list.remove(it) }
rendererComponent = null
return
}
val cellValue = list.model.getElementAt(index)
val selected = list.isSelectedIndex(index)
val focused = list.hasFocus() && selected
rendererComponent = cellRenderer.getListCellRendererComponent(list, cellValue, index, selected, focused).apply {
list.add(this)
bounds = list.getCellBounds(index, index)
validate()
repaint()
}
}
companion object {
/**
* [cellRenderer] should be an instance different from one in the list
*/
fun <T> install(list: JList<T>, cellRenderer: ListCellRenderer<T>): JListHoveredRowMaterialiser<T> {
if (list.cellRenderer === cellRenderer
|| (list.cellRenderer as? ExpandedItemListCellRendererWrapper)?.wrappee === cellRenderer)
error("cellRenderer should be an instance different from list cell renderer")
return JListHoveredRowMaterialiser(list, cellRenderer).also {
with(list) {
addMouseMotionListener(it.listRowHoverListener)
addFocusListener(it.listPresentationListener)
addComponentListener(it.listPresentationListener)
addListSelectionListener(it.listPresentationListener)
model.addListDataListener(it.listDataListener)
}
}
}
}
}