diff --git a/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHCloneDialogAccountsPanel.kt b/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHCloneDialogAccountsPanel.kt deleted file mode 100644 index 92936beb1d59..000000000000 --- a/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHCloneDialogAccountsPanel.kt +++ /dev/null @@ -1,114 +0,0 @@ -// 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 org.jetbrains.plugins.github.ui.cloneDialog - -import com.intellij.ide.BrowserUtil -import com.intellij.openapi.ui.JBPopupMenu -import com.intellij.ui.SizedIcon -import com.intellij.ui.components.JBMenu -import com.intellij.util.IconUtil -import com.intellij.util.ui.JBUI -import com.intellij.util.ui.cloneDialog.VcsCloneDialogUiSpec -import icons.GithubIcons -import org.jetbrains.plugins.github.api.GithubApiRequestExecutorManager -import org.jetbrains.plugins.github.api.data.GithubUser -import org.jetbrains.plugins.github.authentication.GithubAuthenticationManager -import org.jetbrains.plugins.github.authentication.accounts.GithubAccount -import org.jetbrains.plugins.github.pullrequest.avatars.CachingGithubAvatarIconsProvider -import org.jetbrains.plugins.github.util.CachingGithubUserAvatarLoader -import org.jetbrains.plugins.github.util.GithubImageResizer -import java.awt.FlowLayout -import java.awt.event.MouseAdapter -import java.awt.event.MouseEvent -import javax.swing.Icon -import javax.swing.JLabel -import javax.swing.JPanel - -internal class GHCloneDialogAccountsPanel( - private val loginController: GHLoginController, - private val authenticationManager: GithubAuthenticationManager, - private val apiRequestExecutorManager: GithubApiRequestExecutorManager, - private val avatarLoader: CachingGithubUserAvatarLoader, - private val imageResizer: GithubImageResizer -) : JPanel(FlowLayout(FlowLayout.LEADING, JBUI.scale(1), 0)) { - - private val avatarSize = VcsCloneDialogUiSpec.Components.avatarSize - private val defaultIcon = SizedIcon(GithubIcons.DefaultAvatar, avatarSize.get(), avatarSize.get()) - - private val accountComponents = HashMap() - private val userDetails = HashMap>() - - private val popupMenuMouseAdapter = object : MouseAdapter() { - override fun mouseClicked(e: MouseEvent?) = showPopupMenu() - } - - init { - addMouseListener(popupMenuMouseAdapter) - } - - fun addAccount(account: GithubAccount) { - if (accountComponents.isEmpty()) removeAll() - - val label = accountComponents.getOrPut(account, { - JLabel().apply { - icon = defaultIcon - toolTipText = account.name - isOpaque = !isOpaque - addMouseListener(popupMenuMouseAdapter) - } - }) - add(label) - } - - fun removeAccount(removedAccount: GithubAccount) { - remove(accountComponents.remove(removedAccount)) - userDetails.remove(removedAccount) - - revalidate() - repaint() - } - - fun updateUserDetails(account: GithubAccount, user: GithubUser) { - val iconsProvider = CachingGithubAvatarIconsProvider.Factory( - avatarLoader, - imageResizer, - apiRequestExecutorManager.getExecutor(account) - ).create(avatarSize, this) - val avatar = iconsProvider.getIcon(user.avatarUrl) - userDetails[account] = Pair(user, avatar) - accountComponents[account]?.icon = avatar - } - - private fun showPopupMenu() { - // TODO: replace with custom popup action list - val popupMenu = JBPopupMenu() - for (account in authenticationManager.getAccounts()) { - val accountSubmenu = JBMenu().apply { - val pair = userDetails[account] - if (pair == null) { - text = account.name - icon = scaleIcon(defaultIcon) - add("Log in").addActionListener { loginController.reLogin(account) } - addSeparator() - add("Remove account").addActionListener { loginController.logout(account) } - } - else { - val (user, avatar) = pair - text = if (account.server.isGithubDotCom) account.name else ("${account.server.host}/${user.login}") - icon = scaleIcon(avatar) - add("Open on GitHub").addActionListener { BrowserUtil.browse(user.htmlUrl) } - addSeparator() - add("Log Out\u2026").addActionListener { loginController.logout(account) } - } - } - popupMenu.add(accountSubmenu) - } - popupMenu.addSeparator() - popupMenu.add("Add Account\u2026").addActionListener { loginController.addAccount() } - popupMenu.show(this, 0, bounds.maxY.toInt()) - } - - private fun scaleIcon(icon: Icon): Icon { - val scale = JBUI.scale(20).toFloat() / icon.iconWidth.toFloat() - return IconUtil.scale(icon, null, scale) - } -} \ No newline at end of file diff --git a/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHCloneDialogExtensionComponent.kt b/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHCloneDialogExtensionComponent.kt index dc37d32d5df7..88e281410872 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHCloneDialogExtensionComponent.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHCloneDialogExtensionComponent.kt @@ -5,6 +5,8 @@ import com.intellij.dvcs.repo.ClonePathProvider import com.intellij.dvcs.ui.CloneDvcsValidationUtils import com.intellij.dvcs.ui.DvcsBundle.getString import com.intellij.dvcs.ui.SelectChildTextFieldWithBrowseButton +import com.intellij.icons.AllIcons +import com.intellij.ide.BrowserUtil import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.runInEdt @@ -14,6 +16,7 @@ import com.intellij.openapi.progress.EmptyProgressIndicator import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.rd.attachChild import com.intellij.openapi.ui.DialogPanel import com.intellij.openapi.ui.ValidationInfo @@ -27,15 +30,23 @@ import com.intellij.ui.components.panels.Wrapper import com.intellij.ui.layout.* import com.intellij.ui.speedSearch.NameFilteringListModel import com.intellij.ui.speedSearch.SpeedSearch +import com.intellij.util.IconUtil import com.intellij.util.containers.ContainerUtil import com.intellij.util.progress.ProgressVisibilityManager import com.intellij.util.ui.JBEmptyBorder import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil +import com.intellij.util.ui.cloneDialog.AccountMenuItem +import com.intellij.util.ui.cloneDialog.AccountMenuItem.Account +import com.intellij.util.ui.cloneDialog.AccountMenuItem.Action +import com.intellij.util.ui.cloneDialog.AccountMenuPopupStep +import com.intellij.util.ui.cloneDialog.AccountsMenuListPopup +import com.intellij.util.ui.cloneDialog.VcsCloneDialogUiSpec import git4idea.GitUtil import git4idea.checkout.GitCheckoutProvider import git4idea.commands.Git import git4idea.remote.GitRememberedInputs +import icons.GithubIcons import org.jetbrains.plugins.github.api.* import org.jetbrains.plugins.github.api.data.GithubAuthenticatedUser import org.jetbrains.plugins.github.api.data.GithubRepo @@ -46,12 +57,18 @@ import org.jetbrains.plugins.github.authentication.GithubAuthenticationManager import org.jetbrains.plugins.github.authentication.accounts.* import org.jetbrains.plugins.github.authentication.ui.GithubLoginPanel import org.jetbrains.plugins.github.exceptions.GithubMissingTokenException +import org.jetbrains.plugins.github.pullrequest.avatars.CachingGithubAvatarIconsProvider import org.jetbrains.plugins.github.util.CachingGithubUserAvatarLoader import org.jetbrains.plugins.github.util.GithubImageResizer import org.jetbrains.plugins.github.util.GithubUrlUtil import org.jetbrains.plugins.github.util.handleOnEdt +import java.awt.FlowLayout import java.awt.event.ActionListener +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent import java.nio.file.Paths +import javax.swing.Icon +import javax.swing.JLabel import javax.swing.JPanel import javax.swing.JSeparator import javax.swing.event.DocumentEvent @@ -63,29 +80,28 @@ internal class GHCloneDialogExtensionComponent( private val executorManager: GithubApiRequestExecutorManager, private val apiExecutorFactory: GithubApiRequestExecutor.Factory, private val accountInformationProvider: GithubAccountInformationProvider, - avatarLoader: CachingGithubUserAvatarLoader, - imageResizer: GithubImageResizer + private val avatarLoader: CachingGithubUserAvatarLoader, + private val imageResizer: GithubImageResizer ) : VcsCloneDialogExtensionComponent() { private val LOG = logger() private val progressManager: ProgressVisibilityManager // UI + private val defaultAvatar = resizeIcon(GithubIcons.DefaultAvatar, VcsCloneDialogUiSpec.Components.avatarSize.get()) + private val defaultPopupAvatar = resizeIcon(GithubIcons.DefaultAvatar, VcsCloneDialogUiSpec.Components.popupMenuAvatarSize.get()) + private val wrapper: Wrapper = Wrapper() private val repositoriesPanel: DialogPanel private val repositoryList: GHRepositoryList - private val accountsPanel: GHCloneDialogAccountsPanel = GHCloneDialogAccountsPanel( - object : GHLoginController { - override fun addAccount() = switchToLogin() - override fun reLogin(account: GithubAccount) = switchToLogin(account) - override fun logout(account: GithubAccount) = authenticationManager.removeAccount(account) - }, - authenticationManager, - executorManager, - avatarLoader, - imageResizer - ) + private val popupMenuMouseAdapter = object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent?) = showPopupMenu() + } + + private val accountsPanel: JPanel = JPanel(FlowLayout(FlowLayout.LEADING, JBUI.scale(1), 0)).apply { + addMouseListener(popupMenuMouseAdapter) + } private val searchField: SearchTextField = SearchTextField(false).apply { textEditor.emptyText.appendText("Search or enter a GitHub repository URL") @@ -109,6 +125,10 @@ internal class GHCloneDialogExtensionComponent( private var inLoginState = false private var selectedUrl by Delegates.observable(null) { _, _, _ -> onSelectedUrlChanged() } + // popup menu + private val accountComponents = hashMapOf() + private val avatarsByAccount = hashMapOf() + init { val speedSearch = SpeedSearch() @@ -214,7 +234,16 @@ internal class GHCloneDialogExtensionComponent( private fun addAccount(account: GithubAccount) { repositoriesByAccount.remove(account) - accountsPanel.addAccount(account) + + val label = accountComponents.getOrPut(account) { + JLabel().apply { + icon = defaultAvatar + toolTipText = account.name + isOpaque = !isOpaque + addMouseListener(popupMenuMouseAdapter) + } + } + accountsPanel.add(label) try { val executor = executorManager.getExecutor(account) @@ -232,7 +261,11 @@ internal class GHCloneDialogExtensionComponent( private fun removeAccount(account: GithubAccount) { repositoriesByAccount.remove(account) - accountsPanel.removeAccount(account) + accountComponents.remove(account).let { + accountsPanel.remove(it) + accountsPanel.revalidate() + accountsPanel.repaint() + } refillRepositories() if (!authenticationManager.hasAccounts()) switchToLogin() } @@ -241,14 +274,20 @@ internal class GHCloneDialogExtensionComponent( executor: GithubApiRequestExecutor.WithTokenAuth) { progressManager.run(object : Task.Backgroundable(project, "Not Visible") { lateinit var user: GithubAuthenticatedUser + lateinit var iconProvider: CachingGithubAvatarIconsProvider override fun run(indicator: ProgressIndicator) { user = accountInformationProvider.getInformation(executor, indicator, account) + iconProvider = CachingGithubAvatarIconsProvider + .Factory(avatarLoader, imageResizer, executor) + .create(VcsCloneDialogUiSpec.Components.popupMenuAvatarSize, accountsPanel) } override fun onSuccess() { userDetailsByAccount[account] = user - accountsPanel.updateUserDetails(account, user) + val avatar = iconProvider.getIcon(user.avatarUrl) + avatarsByAccount[account] = avatar + accountComponents[account]?.icon = resizeIcon(avatar, VcsCloneDialogUiSpec.Components.avatarSize.get()) refillRepositories() } @@ -452,4 +491,41 @@ internal class GHCloneDialogExtensionComponent( return !wasThere } } + + private fun resizeIcon(icon: Icon, size: Int): Icon { + val scale = JBUI.scale(size).toFloat() / icon.iconWidth.toFloat() + return IconUtil.scale(icon, null, scale) + } + + private fun showPopupMenu() { + val menuItems = mutableListOf() + val project = ProjectManager.getInstance().defaultProject + + for ((index, account) in authenticationManager.getAccounts().withIndex()) { + val user = userDetailsByAccount[account] + + val accountTitle = user?.login ?: account.name + val serverInfo = account.server.toUrl().removePrefix("http://").removePrefix("https://") + val avatar = avatarsByAccount[account] ?: defaultPopupAvatar + val accountActions = mutableListOf() + val showSeparatorAbove = index != 0 + + if (user == null) { + accountActions += Action("Log in\u2026", { switchToLogin(account) }) + accountActions += Action("Remove account", { authenticationManager.removeAccount(account) }, showSeparatorAbove = true) + } + else { + if (account != authenticationManager.getDefaultAccount(project)) { + accountActions += Action("Set as Default", { authenticationManager.setDefaultAccount(project, account) }) + } + accountActions += Action("Open on GitHub", { BrowserUtil.browse(user.htmlUrl) }, AllIcons.Ide.External_link_arrow) + accountActions += Action("Log Out\u2026", { authenticationManager.removeAccount(account) }, showSeparatorAbove = true) + } + + menuItems += Account(accountTitle, serverInfo, avatar, accountActions, showSeparatorAbove) + } + menuItems += Action("Add Account\u2026", { switchToLogin() }, showSeparatorAbove = true) + + AccountsMenuListPopup(null, AccountMenuPopupStep(menuItems)).showUnderneathOf(accountsPanel) + } } diff --git a/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHLoginController.kt b/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHLoginController.kt deleted file mode 100644 index 6b46d29c9432..000000000000 --- a/plugins/github/src/org/jetbrains/plugins/github/ui/cloneDialog/GHLoginController.kt +++ /dev/null @@ -1,12 +0,0 @@ -// 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 org.jetbrains.plugins.github.ui.cloneDialog - -import org.jetbrains.plugins.github.authentication.accounts.GithubAccount - -internal interface GHLoginController { - fun addAccount() - - fun reLogin(account: GithubAccount) - - fun logout(account: GithubAccount) -} \ No newline at end of file