mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[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:
committed by
intellij-monorepo-bot
parent
d6440d2d13
commit
c886fb8582
+76
-169
@@ -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
|
||||
|
||||
+105
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user