mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-10-02 10:14:57 +07:00
[github] Multiple accounts configuration
This commit is contained in:
@@ -111,6 +111,10 @@ public class CollectionListModel<T> extends AbstractListModel<T> implements Edit
|
||||
fireContentsChanged(this, i, i);
|
||||
}
|
||||
|
||||
public void allContentsChanged() {
|
||||
fireContentsChanged(this, 0, myItems.size() - 1);
|
||||
}
|
||||
|
||||
public void sort(final Comparator<T> comparator) {
|
||||
Collections.sort(myItems, comparator);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 49.3 (51167) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>github/resources/org/jetbrains/plugins/github/GitHubAvatar_40</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="github/resources/org/jetbrains/plugins/github/GitHubAvatar_40" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" fill-opacity="0.5">
|
||||
<g id="avatar" transform="translate(6.000000, 6.000000)" fill="#7F8B91" fill-rule="nonzero">
|
||||
<path d="M14,14 C17.6833333,14 20.6666667,11.0166667 20.6666667,7.33333333 C20.6666667,3.65 17.6833333,0.666666667 14,0.666666667 C10.3166667,0.666666667 7.33333333,3.65 7.33333333,7.33333333 C7.33333333,11.0166667 10.3166667,14 14,14 Z M14,17.3333333 C9.55,17.3333333 0.666666667,19.5666667 0.666666667,24 L0.666666667,27.3333333 L27.3333333,27.3333333 L27.3333333,24 C27.3333333,19.5666667 18.45,17.3333333 14,17.3333333 Z" id="Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -24,11 +24,12 @@
|
||||
<applicationService serviceImplementation="org.jetbrains.plugins.github.authentication.accounts.GithubAccountManager"/>
|
||||
<applicationService serviceImplementation="org.jetbrains.plugins.github.authentication.GithubAuthenticationManager"/>
|
||||
<applicationService serviceImplementation="org.jetbrains.plugins.github.api.GithubApiTaskExecutor"/>
|
||||
<applicationService serviceImplementation="org.jetbrains.plugins.github.authentication.accounts.GithubAccountInformationProvider"/>
|
||||
<projectService serviceInterface="org.jetbrains.plugins.github.util.GithubProjectSettings"
|
||||
serviceImplementation="org.jetbrains.plugins.github.util.GithubProjectSettings"/>
|
||||
<projectService serviceImplementation="org.jetbrains.plugins.github.authentication.accounts.GithubProjectDefaultAccountHolder"/>
|
||||
<vcsAnnotationGutterActionProvider implementation="org.jetbrains.plugins.github.extensions.GithubAnnotationGutterActionProvider"/>
|
||||
<vcsConfigurableProvider implementation="org.jetbrains.plugins.github.ui.GithubSettingsConfigurable"/>
|
||||
<projectConfigurable parentId="project.propVCSSupport.Mappings" instance="org.jetbrains.plugins.github.ui.GithubSettingsConfigurable"/>
|
||||
</extensions>
|
||||
|
||||
<extensions defaultExtensionNs="Git4Idea">
|
||||
|
||||
@@ -28,5 +28,6 @@ public class GithubIcons {
|
||||
return IconLoader.getIcon(path, GithubIcons.class);
|
||||
}
|
||||
|
||||
public static final Icon DefaultAvatar_40 = load("/org/jetbrains/plugins/github/defaultAvatar_40.svg"); // 40x40
|
||||
public static final Icon Github_icon = load("/org/jetbrains/plugins/github/github_icon.png"); // 16x16
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@ import com.intellij.util.xmlb.annotations.Tag;
|
||||
import org.jetbrains.annotations.NonNls;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.plugins.github.exceptions.GithubParseException;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
@@ -51,8 +56,54 @@ public class GithubServerPath {
|
||||
return mySuffix;
|
||||
}
|
||||
|
||||
// 2 - host, 4 - port, 5 - path
|
||||
private final static Pattern URL_REGEX = Pattern.compile("^(https?://)?([^/?:]+)(:(\\d+))?((/[^/?#]+)*)?");
|
||||
|
||||
@NotNull
|
||||
public static GithubServerPath from(@NotNull String uri) throws GithubParseException {
|
||||
Matcher matcher = URL_REGEX.matcher(uri);
|
||||
|
||||
if (!matcher.matches()) throw new GithubParseException("Not a valid URL");
|
||||
String host = matcher.group(2);
|
||||
if (host == null) throw new GithubParseException("Empty host");
|
||||
|
||||
Integer port;
|
||||
String portGroup = matcher.group(4);
|
||||
if (portGroup == null) {
|
||||
port = null;
|
||||
}
|
||||
else {
|
||||
try {
|
||||
port = Integer.parseInt(portGroup);
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
throw new GithubParseException("Invalid port format");
|
||||
}
|
||||
}
|
||||
|
||||
String path = StringUtil.nullize(matcher.group(5));
|
||||
|
||||
return new GithubServerPath(host, port, path);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
String port = myPort != null ? (":" + myPort.toString()) : "";
|
||||
return myHost + port + StringUtil.notNullize(mySuffix);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof GithubServerPath)) return false;
|
||||
GithubServerPath path = (GithubServerPath)o;
|
||||
return Objects.equals(myHost, path.myHost) &&
|
||||
Objects.equals(myPort, path.myPort) &&
|
||||
Objects.equals(mySuffix, path.mySuffix);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
|
||||
return Objects.hash(myHost, myPort, mySuffix);
|
||||
}
|
||||
}
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
// Copyright 2000-2018 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.authentication.accounts
|
||||
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import org.jetbrains.plugins.github.api.GithubApiTaskExecutor
|
||||
import org.jetbrains.plugins.github.api.GithubTask
|
||||
import org.jetbrains.plugins.github.api.GithubApiUtil
|
||||
import org.jetbrains.plugins.github.api.GithubServerPath
|
||||
import org.jetbrains.plugins.github.api.data.GithubUserDetailed
|
||||
import java.awt.Image
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import javax.imageio.ImageIO
|
||||
|
||||
//TODO: caching
|
||||
//TODO: load image with GithubApiTaskExecutor
|
||||
class GithubAccountInformationProvider(private val apiTaskExecutor: GithubApiTaskExecutor) {
|
||||
@Throws(IOException::class)
|
||||
fun getAccountInformationWithPicture(indicator: ProgressIndicator, server: GithubServerPath, token: String): Pair<GithubUserDetailed, Image> {
|
||||
val details = GithubApiTaskExecutor.execute(indicator, server, token, GithubTask { c -> GithubApiUtil.getCurrentUser(c) })
|
||||
return details to ImageIO.read(URL(details.avatarUrl))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getAccountInformationWithPicture(indicator: ProgressIndicator, account: GithubAccount): Pair<GithubUserDetailed, Image> {
|
||||
val details = getAccountInformation(indicator, account)
|
||||
return details to ImageIO.read(URL(details.avatarUrl))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getAccountInformation(indicator: ProgressIndicator, account: GithubAccount): GithubUserDetailed {
|
||||
return apiTaskExecutor.execute(indicator, account, GithubTask { c -> GithubApiUtil.getCurrentUser(c) })
|
||||
}
|
||||
}
|
||||
+386
@@ -0,0 +1,386 @@
|
||||
// Copyright 2000-2018 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.authentication.ui
|
||||
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.CommonShortcuts
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.keymap.KeymapUtil
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.progress.Task
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.ui.*
|
||||
import com.intellij.ui.SimpleTextAttributes.STYLE_PLAIN
|
||||
import com.intellij.ui.SimpleTextAttributes.STYLE_UNDERLINE
|
||||
import com.intellij.ui.components.JBList
|
||||
import com.intellij.util.Alarm
|
||||
import com.intellij.util.AlarmFactory
|
||||
import com.intellij.util.progress.ProgressVisibilityManager
|
||||
import com.intellij.util.ui.*
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import icons.GithubIcons
|
||||
import org.jetbrains.plugins.github.api.GithubServerPath
|
||||
import org.jetbrains.plugins.github.api.data.GithubUserDetailed
|
||||
import org.jetbrains.plugins.github.authentication.accounts.GithubAccount
|
||||
import org.jetbrains.plugins.github.authentication.accounts.GithubAccountInformationProvider
|
||||
import org.jetbrains.plugins.github.exceptions.GithubAuthenticationException
|
||||
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 ACCOUNT_PICTURE_SIZE: Int = 40
|
||||
|
||||
internal class GithubAccountsPanel(private val project: Project, private val accountInformationProvider: GithubAccountInformationProvider)
|
||||
: 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.loadingError != null })
|
||||
override fun intervalRemoved(e: ListDataEvent?) {}
|
||||
override fun intervalAdded(e: ListDataEvent?) {}
|
||||
})
|
||||
}
|
||||
private val accountList = JBList<GithubAccountDecorator>(accountListModel).apply {
|
||||
cellRenderer = GithubAccountDecoratorRenderer()
|
||||
selectionMode = ListSelectionModel.SINGLE_SELECTION
|
||||
selectionForeground = UIUtil.getListForeground()
|
||||
selectionBackground = JBColor(0xE9EEF5, 0x464A4D)
|
||||
emptyText.apply {
|
||||
appendText("No GitHub accounts added.")
|
||||
appendSecondaryText("Add account", SimpleTextAttributes.LINK_ATTRIBUTES, { addAccount() })
|
||||
appendSecondaryText(" (${KeymapUtil.getFirstKeyboardShortcutText(CommonShortcuts.getNew())})", StatusText.DEFAULT_ATTRIBUTES, null)
|
||||
}
|
||||
}
|
||||
|
||||
private val progressManager = createListProgressManager()
|
||||
private val errorLinkHandler = createLinkActivationListener()
|
||||
private var errorLinkHandlerInstalled = false
|
||||
private val newTokensMap = mutableMapOf<GithubAccount, String>()
|
||||
|
||||
init {
|
||||
addToCenter(ToolbarDecorator.createDecorator(accountList)
|
||||
.disableUpDownActions()
|
||||
.setPanelBorder(IdeBorderFactory.createBorder(SideBorder.TOP or SideBorder.BOTTOM))
|
||||
.setAddAction { addAccount() }
|
||||
.addExtraAction(object : ToolbarDecorator.ElementActionButton("Set default",
|
||||
AllIcons.Actions.Checked) {
|
||||
override fun actionPerformed(e: AnActionEvent?) {
|
||||
if (accountList.selectedValue.projectDefault) return
|
||||
for (accountData in accountListModel.items) {
|
||||
if (accountData == accountList.selectedValue) {
|
||||
accountData.projectDefault = true
|
||||
accountListModel.contentsChanged(accountData)
|
||||
}
|
||||
else if (accountData.projectDefault) {
|
||||
accountData.projectDefault = false
|
||||
accountListModel.contentsChanged(accountData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateButton(e: AnActionEvent?) {
|
||||
isEnabled = isEnabled && !accountList.selectedValue.projectDefault
|
||||
}
|
||||
})
|
||||
.createPanel())
|
||||
|
||||
Disposer.register(this, progressManager)
|
||||
Disposer.register(this, errorLinkHandler)
|
||||
}
|
||||
|
||||
private fun addAccount() {
|
||||
val dialog = GithubLoginDialog(project,
|
||||
::isAccountUnique,
|
||||
this)
|
||||
if (dialog.showAndGet()) {
|
||||
val githubAccount = GithubAccount(dialog.getLogin(), dialog.getServer())
|
||||
newTokensMap[githubAccount] = dialog.getToken()
|
||||
|
||||
val accountData = GithubAccountDecorator(githubAccount, false)
|
||||
accountListModel.add(accountData)
|
||||
loadAccountDetails(accountData)
|
||||
}
|
||||
}
|
||||
|
||||
private fun editAccount(decorator: GithubAccountDecorator) {
|
||||
val dialog = GithubLoginDialog(project,
|
||||
{ _, _ -> true },
|
||||
this,
|
||||
host = decorator.account.server.toString(),
|
||||
editableHost = false)
|
||||
if (dialog.showAndGet()) {
|
||||
decorator.account.name = dialog.getLogin()
|
||||
newTokensMap[decorator.account] = dialog.getToken()
|
||||
loadAccountDetails(decorator)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
* Mouse movement handling is performed with delay to reduce performance impact
|
||||
*
|
||||
* @see [GithubAccountDecorator.loadingError]
|
||||
* @see [GithubAccountDecorator.showLoginLink]
|
||||
* @see [GithubAccountDecorator.errorLinkPointedAt]
|
||||
*/
|
||||
private fun createLinkActivationListener() = object : MouseAdapter(), Disposable {
|
||||
// Display refresh delay at 25 fps
|
||||
private val DELAY_MS: Long = 40L
|
||||
private var linkStylingAlarm = AlarmFactory.getInstance()
|
||||
.create(Alarm.ThreadToUse.SWING_THREAD, this)
|
||||
.apply { setActivationComponent(accountList) }
|
||||
|
||||
override fun mouseMoved(e: MouseEvent) {
|
||||
linkStylingAlarm.cancelAllRequests()
|
||||
linkStylingAlarm.addComponentRequest(
|
||||
{
|
||||
for (item in accountListModel.items) {
|
||||
item.errorLinkPointedAt = false
|
||||
}
|
||||
val (decorator, linkPointedAt) = findDecoratorWithLoginLinkAt(e.point)
|
||||
if (decorator != null) {
|
||||
UIUtil.setCursor(accountList,
|
||||
if (linkPointedAt) Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) else Cursor.getDefaultCursor())
|
||||
decorator.errorLinkPointedAt = linkPointedAt
|
||||
}
|
||||
else {
|
||||
UIUtil.setCursor(accountList, Cursor.getDefaultCursor())
|
||||
}
|
||||
|
||||
accountListModel.allContentsChanged()
|
||||
}, DELAY_MS)
|
||||
}
|
||||
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
val (decorator, linkPointedAt) = findDecoratorWithLoginLinkAt(e.point)
|
||||
if (decorator != null && linkPointedAt) editAccount(decorator)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds component under mouse pointer and if it's a decorator - checks if pointer is on the error label link
|
||||
*/
|
||||
private fun findDecoratorWithLoginLinkAt(point: Point): Pair<GithubAccountDecorator?, Boolean> {
|
||||
val idx = accountList.locationToIndex(point)
|
||||
if (idx < 0) return null to false
|
||||
|
||||
val cellBounds = accountList.getCellBounds(idx, idx)
|
||||
if (!cellBounds.contains(point)) return null to false
|
||||
|
||||
val decorator = accountListModel.getElementAt(idx)
|
||||
if (decorator?.loadingError == null) return null to false
|
||||
|
||||
val rendererComponent = accountList.cellRenderer.getListCellRendererComponent(accountList, decorator, idx, true, true)
|
||||
rendererComponent.setLocation(cellBounds.x, cellBounds.y)
|
||||
rendererComponent.setSize(cellBounds.width, cellBounds.height)
|
||||
|
||||
val rendererRelativePoint = Point(point.x - cellBounds.x, point.y - cellBounds.y)
|
||||
val childComponent = UIUtil.getDeepestComponentAt(rendererComponent, rendererRelativePoint.x, rendererRelativePoint.y)
|
||||
if (childComponent == null || childComponent !is SimpleColoredComponent) return decorator to false
|
||||
|
||||
val childRelativeX = SwingUtilities.convertPoint(rendererComponent, rendererRelativePoint, childComponent).x
|
||||
return if (childComponent.findFragmentAt(childRelativeX) == 2) decorator to true else decorator to false
|
||||
}
|
||||
|
||||
override fun dispose() {}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadAccountDetails(accountData: GithubAccountDecorator) {
|
||||
val newToken: String? = newTokensMap[accountData.account]
|
||||
progressManager.run(object : Task.Backgroundable(project, "Not Visible") {
|
||||
lateinit var data: Pair<GithubUserDetailed, Image>
|
||||
|
||||
override fun run(indicator: ProgressIndicator) {
|
||||
data = if (newToken != null) {
|
||||
accountInformationProvider.getAccountInformationWithPicture(indicator, accountData.account.server, newToken)
|
||||
}
|
||||
else {
|
||||
accountInformationProvider.getAccountInformationWithPicture(indicator, accountData.account)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSuccess() {
|
||||
accountListModel.contentsChanged(accountData.apply {
|
||||
fullName = data.first.name
|
||||
profilePicture = data.second
|
||||
loadingError = null
|
||||
})
|
||||
}
|
||||
|
||||
override fun onThrowable(error: Throwable) {
|
||||
accountListModel.contentsChanged(accountData.apply {
|
||||
loadingError = error.message.toString()
|
||||
showLoginLink = error is GithubAuthenticationException
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun createListProgressManager() = object : ProgressVisibilityManager() {
|
||||
override fun setProgressVisible(visible: Boolean) = accountList.setPaintBusy(visible)
|
||||
override fun getModalityState() = ModalityState.any()
|
||||
}
|
||||
|
||||
fun setAccounts(accounts: Set<GithubAccount>, defaultAccount: GithubAccount?) {
|
||||
accountListModel.removeAll()
|
||||
accountListModel.addAll(0, accounts.map { GithubAccountDecorator(it, it == defaultAccount) })
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list of accounts and associated tokens if new token was created and selected default account
|
||||
*/
|
||||
fun getAccounts(): Pair<Map<GithubAccount, String?>, GithubAccount?> {
|
||||
return accountListModel.items.associate { it.account to newTokensMap[it.account] } to
|
||||
accountListModel.items.find { it.projectDefault }?.account
|
||||
}
|
||||
|
||||
fun clearNewTokens() = newTokensMap.clear()
|
||||
|
||||
fun isModified(accounts: Set<GithubAccount>, defaultAccount: GithubAccount?): Boolean {
|
||||
return accountListModel.items.find { it.projectDefault }?.account != defaultAccount ||
|
||||
accountListModel.items.map { it.account }.toSet() != accounts ||
|
||||
newTokensMap.isNotEmpty()
|
||||
}
|
||||
|
||||
override fun dispose() {}
|
||||
}
|
||||
|
||||
private class GithubAccountDecoratorRenderer : ListCellRenderer<GithubAccountDecorator>, JPanel() {
|
||||
private val accountName = JLabel()
|
||||
|
||||
private val serverName = JLabel()
|
||||
private val profilePicture = JLabel()
|
||||
|
||||
private val fullName = JLabel()
|
||||
|
||||
private val loadingError = SimpleColoredComponent()
|
||||
|
||||
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 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, if (isSelected) list.selectionBackground else list.background)
|
||||
|
||||
val textColor = if (isSelected) list.selectionForeground else list.foreground
|
||||
val grayTextColor = if (isSelected) list.selectionForeground else Gray._120
|
||||
|
||||
accountName.apply {
|
||||
text = value.account.name
|
||||
setBold(if (value.fullName == null) value.projectDefault else false)
|
||||
foreground = if (value.fullName == null) textColor else grayTextColor
|
||||
}
|
||||
serverName.apply {
|
||||
text = value.account.server.toString()
|
||||
foreground = grayTextColor
|
||||
}
|
||||
profilePicture.apply {
|
||||
icon = value.profilePicture?.let {
|
||||
val size = JBUI.scale(ACCOUNT_PICTURE_SIZE)
|
||||
JBImageIcon(it.getScaledInstance(size, size, java.awt.Image.SCALE_FAST))
|
||||
} ?: GithubIcons.DefaultAvatar_40
|
||||
}
|
||||
fullName.apply {
|
||||
text = value.fullName
|
||||
setBold(value.projectDefault)
|
||||
isVisible = value.fullName != null
|
||||
foreground = textColor
|
||||
}
|
||||
loadingError.apply {
|
||||
clear()
|
||||
value.loadingError?.let {
|
||||
append(it, SimpleTextAttributes.ERROR_ATTRIBUTES)
|
||||
append(" ")
|
||||
if (value.showLoginLink) append("Log In",
|
||||
if (value.errorLinkPointedAt) SimpleTextAttributes(STYLE_UNDERLINE, JBColor.link())
|
||||
else SimpleTextAttributes(STYLE_PLAIN, JBColor.link()))
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun JLabel.setBold(isBold: Boolean) {
|
||||
font = font.deriveFont(if (isBold) font.style or Font.BOLD else font.style and Font.BOLD.inv())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Account + auxillary info + info loading error
|
||||
*/
|
||||
private class GithubAccountDecorator(val account: GithubAccount, var projectDefault: Boolean) {
|
||||
var fullName: String? = null
|
||||
var profilePicture: Image? = null
|
||||
var loadingError: String? = null
|
||||
|
||||
var showLoginLink = false
|
||||
var errorLinkPointedAt = false
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as GithubAccountDecorator
|
||||
|
||||
if (account != other.account) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return account.hashCode()
|
||||
}
|
||||
}
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
// Copyright 2000-2018 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.authentication.ui
|
||||
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ide.BrowserUtil
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.progress.EmptyProgressIndicator
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.progress.Task
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.DialogWrapper
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.ui.AnimatedIcon
|
||||
import com.intellij.ui.JBColor
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBPasswordField
|
||||
import com.intellij.ui.components.JBTextField
|
||||
import com.intellij.ui.components.fields.ExtendableTextField
|
||||
import com.intellij.ui.components.labels.LinkLabel
|
||||
import com.intellij.ui.components.panels.Wrapper
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UI.PanelFactory.grid
|
||||
import com.intellij.util.ui.UI.PanelFactory.panel
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import org.jetbrains.plugins.github.api.GithubApiTaskExecutor
|
||||
import org.jetbrains.plugins.github.api.GithubApiUtil
|
||||
import org.jetbrains.plugins.github.api.GithubServerPath
|
||||
import org.jetbrains.plugins.github.api.GithubTask
|
||||
import org.jetbrains.plugins.github.exceptions.GithubAuthenticationException
|
||||
import org.jetbrains.plugins.github.exceptions.GithubParseException
|
||||
import org.jetbrains.plugins.github.ui.util.DialogValidationUtils.chain
|
||||
import org.jetbrains.plugins.github.ui.util.DialogValidationUtils.notBlank
|
||||
import org.jetbrains.plugins.github.ui.util.Validator
|
||||
import org.jetbrains.plugins.github.util.GithubUtil
|
||||
import java.awt.Cursor
|
||||
import java.net.UnknownHostException
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JEditorPane
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.JTextField
|
||||
import javax.swing.event.HyperlinkEvent
|
||||
import javax.swing.text.html.HTMLEditorKit
|
||||
|
||||
class GithubLoginDialog(private val project: Project,
|
||||
private val isAccountUnique: (name: String, server: GithubServerPath) -> Boolean,
|
||||
parent: JComponent? = null,
|
||||
title: String = "Log In to Github",
|
||||
host: String = GithubApiUtil.DEFAULT_GITHUB_HOST,
|
||||
editableHost: Boolean = true,
|
||||
login: String = "",
|
||||
private val tokenNote: String = GithubUtil.DEFAULT_TOKEN_NOTE)
|
||||
: DialogWrapper(project, parent, false, IdeModalityType.PROJECT) {
|
||||
|
||||
private val serverTextField = ExtendableTextField(host).apply { isEditable = editableHost }
|
||||
private var centerPanel = Wrapper()
|
||||
|
||||
private var southAdditionalPanel = Wrapper().apply { JBUI.Borders.emptyRight(UIUtil.DEFAULT_HGAP) }
|
||||
|
||||
private var passwordUi = LoginPasswordCredentialsUI(login)
|
||||
private var tokenUi = TokenCredentialsUI()
|
||||
|
||||
private lateinit var currentUi: CredentialsUI
|
||||
|
||||
private var progressIndicator: ProgressIndicator? = null
|
||||
private val progressIcon = AnimatedIcon.Default()
|
||||
private val progressExtension = ExtendableTextField.Extension({ progressIcon })
|
||||
|
||||
private lateinit var login: String
|
||||
private lateinit var token: String
|
||||
private var tokenAcquisitionError: ValidationInfo? = null
|
||||
|
||||
init {
|
||||
this.title = title
|
||||
setOKButtonText("Log In")
|
||||
applyUi(passwordUi)
|
||||
init()
|
||||
Disposer.register(disposable, Disposable { progressIndicator?.cancel() })
|
||||
}
|
||||
|
||||
fun getServer(): GithubServerPath = GithubServerPath.from(serverTextField.text)
|
||||
fun getLogin(): String = login
|
||||
fun getToken(): String = token
|
||||
|
||||
override fun doOKAction() {
|
||||
setBusy(true)
|
||||
tokenAcquisitionError = null
|
||||
|
||||
service<ProgressManager>().runProcessWithProgressAsynchronously(object : Task.Backgroundable(project, "Not Visible") {
|
||||
override fun run(indicator: ProgressIndicator) {
|
||||
val (newLogin, newToken) = currentUi.acquireLoginAndToken(indicator)
|
||||
login = newLogin
|
||||
token = newToken
|
||||
}
|
||||
|
||||
override fun onSuccess() {
|
||||
close(OK_EXIT_CODE, true)
|
||||
setBusy(false)
|
||||
}
|
||||
|
||||
override fun onThrowable(error: Throwable) {
|
||||
startTrackingValidation()
|
||||
tokenAcquisitionError = currentUi.handleAcquireError(error)
|
||||
setBusy(false)
|
||||
}
|
||||
}, progressIndicator!!)
|
||||
}
|
||||
|
||||
private fun setBusy(busy: Boolean) {
|
||||
if (busy) {
|
||||
if (!serverTextField.extensions.contains(progressExtension)) serverTextField.addExtension(progressExtension)
|
||||
progressIndicator?.cancel()
|
||||
progressIndicator = EmptyProgressIndicator(ModalityState.stateForComponent(window))
|
||||
Disposer.register(disposable, Disposable { progressIndicator?.cancel() })
|
||||
}
|
||||
else {
|
||||
serverTextField.removeExtension(progressExtension)
|
||||
progressIndicator = null
|
||||
}
|
||||
serverTextField.isEnabled = !busy
|
||||
currentUi.setBusy(busy)
|
||||
}
|
||||
|
||||
override fun createSouthAdditionalPanel() = southAdditionalPanel
|
||||
override fun createCenterPanel() = centerPanel
|
||||
override fun getPreferredFocusedComponent() = currentUi.getPreferredFocus()
|
||||
|
||||
private fun applyUi(ui: CredentialsUI) {
|
||||
currentUi = ui
|
||||
centerPanel.setContent(currentUi.getPanel())
|
||||
southAdditionalPanel.setContent(currentUi.getSouthPanel())
|
||||
setErrorText(null)
|
||||
currentUi.getPreferredFocus().requestFocus()
|
||||
tokenAcquisitionError = null
|
||||
}
|
||||
|
||||
override fun doValidateAll(): List<ValidationInfo> {
|
||||
return listOf(chain(chain({ notBlank(serverTextField, "Server cannot be empty") },
|
||||
serverPathValidator(serverTextField)),
|
||||
currentUi.getValidator()),
|
||||
{ tokenAcquisitionError })
|
||||
.mapNotNull { it() }
|
||||
}
|
||||
|
||||
private fun serverPathValidator(textField: JTextField): Validator {
|
||||
return {
|
||||
val text = textField.text
|
||||
try {
|
||||
GithubServerPath.from(text)
|
||||
null
|
||||
}
|
||||
catch (e: Exception) {
|
||||
ValidationInfo("$text is not a valid server path:\n${e.message}", textField)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class LoginPasswordCredentialsUI(login: String) : CredentialsUI() {
|
||||
private val loginTextField = JBTextField(login)
|
||||
private val passwordField = JBPasswordField()
|
||||
private val contextHelp = JEditorPane()
|
||||
|
||||
init {
|
||||
contextHelp.apply {
|
||||
editorKit = UIUtil.getHTMLEditorKit()
|
||||
val linkColor = JBColor.link()
|
||||
//language=CSS
|
||||
(editorKit as HTMLEditorKit).styleSheet.addRule("a {color: rgb(${linkColor.red}, ${linkColor.green}, ${linkColor.blue})}")
|
||||
//language=HTML
|
||||
text = "<html>Login and Password are not saved and used only to <br>acquire Github token. <a href=''>Enter token</a></html>"
|
||||
addHyperlinkListener { e ->
|
||||
if (e.eventType == HyperlinkEvent.EventType.ACTIVATED) {
|
||||
applyUi(tokenUi)
|
||||
}
|
||||
else {
|
||||
cursor = if (e.eventType == HyperlinkEvent.EventType.ENTERED) Cursor(Cursor.HAND_CURSOR)
|
||||
else Cursor(Cursor.DEFAULT_CURSOR)
|
||||
}
|
||||
}
|
||||
isEditable = false
|
||||
isFocusable = false
|
||||
isOpaque = false
|
||||
border = null
|
||||
margin = JBUI.emptyInsets()
|
||||
foreground = JBColor.GRAY
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPanel() = grid()
|
||||
.add(panel(serverTextField).withLabel("Server:"))
|
||||
.add(panel(loginTextField).withLabel("Login:"))
|
||||
.add(panel(passwordField).withLabel("Password:"))
|
||||
.add(panel(contextHelp)).createPanel()
|
||||
|
||||
override fun getPreferredFocus() = loginTextField
|
||||
|
||||
override fun getValidator() = chain({ notBlank(loginTextField, "Login cannot be empty") },
|
||||
{ notBlank(passwordField, "Password cannot be empty") })
|
||||
|
||||
override fun getSouthPanel() = JBUI.Panels.simplePanel()
|
||||
.addToCenter(LinkLabel.create("Sign up for Github", Runnable { BrowserUtil.browse("https://github.com") }))
|
||||
.addToRight(JBLabel(AllIcons.Ide.External_link_arrow))
|
||||
|
||||
override fun acquireLoginAndToken(indicator: ProgressIndicator): Pair<String, String> {
|
||||
val server = getServer()
|
||||
if (!isAccountUnique(loginTextField.text, server)) throw LoginNotUniqueException()
|
||||
|
||||
val token = GithubApiTaskExecutor.execute(indicator, server, loginTextField.text, passwordField.password,
|
||||
GithubTask { GithubApiUtil.getMasterToken(it, tokenNote) })
|
||||
return loginTextField.text to token
|
||||
}
|
||||
|
||||
override fun handleAcquireError(error: Throwable): ValidationInfo {
|
||||
return when (error) {
|
||||
is LoginNotUniqueException -> ValidationInfo("Account already added", loginTextField)
|
||||
is UnknownHostException -> ValidationInfo("Server is unreachable", false)
|
||||
is GithubAuthenticationException -> ValidationInfo("Incorrect credentials.", false)
|
||||
is GithubParseException -> ValidationInfo(error.message ?: "Invalid server path", serverTextField)
|
||||
else -> ValidationInfo("Invalid authentication data.\n ${error.message}", false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setBusy(busy: Boolean) {
|
||||
loginTextField.isEnabled = !busy
|
||||
passwordField.isEnabled = !busy
|
||||
contextHelp.isEnabled = !busy
|
||||
}
|
||||
}
|
||||
|
||||
private inner class TokenCredentialsUI : CredentialsUI() {
|
||||
private val tokenTextField = JBTextField()
|
||||
private val switchUiLink = LinkLabel.create("Log In with Username", { applyUi(passwordUi) })
|
||||
|
||||
override fun getPanel() = grid()
|
||||
.add(panel(serverTextField).withLabel("Server:"))
|
||||
.add(panel(tokenTextField).withLabel("Token:")).createPanel()
|
||||
|
||||
override fun getPreferredFocus() = tokenTextField
|
||||
|
||||
override fun getValidator() = { notBlank(tokenTextField, "Token cannot be empty") }
|
||||
|
||||
override fun getSouthPanel() = switchUiLink
|
||||
|
||||
override fun acquireLoginAndToken(indicator: ProgressIndicator): Pair<String, String> {
|
||||
val login = GithubApiTaskExecutor.execute(indicator, getServer(), tokenTextField.text,
|
||||
GithubTask { GithubApiUtil.getCurrentUser(it).login })
|
||||
|
||||
if (!isAccountUnique(login, getServer())) throw LoginNotUniqueException(login)
|
||||
|
||||
return login to tokenTextField.text
|
||||
}
|
||||
|
||||
override fun handleAcquireError(error: Throwable): ValidationInfo {
|
||||
return when (error) {
|
||||
is LoginNotUniqueException -> ValidationInfo("Account ${error.login} already added", false)
|
||||
is UnknownHostException -> ValidationInfo("Server is unreachable", false)
|
||||
is GithubAuthenticationException -> ValidationInfo("Incorrect credentials.", false)
|
||||
is GithubParseException -> ValidationInfo(error.message ?: "Invalid server path", serverTextField)
|
||||
else -> ValidationInfo("Invalid authentication data.\n ${error.message}", false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setBusy(busy: Boolean) {
|
||||
tokenTextField.isEnabled = !busy
|
||||
switchUiLink.isEnabled = !busy
|
||||
}
|
||||
}
|
||||
|
||||
private abstract inner class CredentialsUI {
|
||||
abstract fun getPanel(): JPanel
|
||||
abstract fun getPreferredFocus(): JComponent
|
||||
abstract fun getSouthPanel(): JComponent
|
||||
abstract fun getValidator(): Validator
|
||||
abstract fun acquireLoginAndToken(indicator: ProgressIndicator): Pair<String, String>
|
||||
abstract fun handleAcquireError(error: Throwable): ValidationInfo
|
||||
abstract fun setBusy(busy: Boolean)
|
||||
}
|
||||
|
||||
private class LoginNotUniqueException(val login: String? = null) : RuntimeException()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright 2000-2018 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.exceptions;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class GithubParseException extends RuntimeException {
|
||||
public GithubParseException(@NotNull String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -1,87 +1,30 @@
|
||||
/*
|
||||
* Copyright 2000-2016 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.jetbrains.plugins.github.ui;
|
||||
// Copyright 2000-2018 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
|
||||
|
||||
import com.intellij.openapi.options.Configurable;
|
||||
import com.intellij.openapi.options.ConfigurationException;
|
||||
import com.intellij.openapi.options.SearchableConfigurable;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.vcs.VcsConfigurableProvider;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.plugins.github.util.GithubUtil;
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.options.Configurable
|
||||
import com.intellij.openapi.options.ConfigurableBase
|
||||
import com.intellij.openapi.project.Project
|
||||
import org.jetbrains.plugins.github.authentication.accounts.GithubAccountManager
|
||||
import org.jetbrains.plugins.github.authentication.accounts.GithubProjectDefaultAccountHolder
|
||||
import org.jetbrains.plugins.github.util.GithubSettings
|
||||
import org.jetbrains.plugins.github.util.GithubUtil
|
||||
|
||||
import javax.swing.*;
|
||||
class GithubSettingsConfigurable(private val project: Project) :
|
||||
ConfigurableBase<GithubSettingsPanel, GithubSettingsConfigurable.GithubSettingsHolder>("settings.github",
|
||||
GithubUtil.SERVICE_DISPLAY_NAME,
|
||||
"settings.github"),
|
||||
Configurable.NoMargin {
|
||||
|
||||
/**
|
||||
* @author oleg
|
||||
*/
|
||||
public class GithubSettingsConfigurable implements SearchableConfigurable, VcsConfigurableProvider {
|
||||
private GithubSettingsPanel mySettingsPane;
|
||||
|
||||
public GithubSettingsConfigurable() {
|
||||
override fun getSettings(): GithubSettingsHolder {
|
||||
return GithubSettingsHolder(service(), service(), project.service())
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getDisplayName() {
|
||||
return GithubUtil.SERVICE_DISPLAY_NAME;
|
||||
override fun createUi(): GithubSettingsPanel {
|
||||
return GithubSettingsPanel(project, service())
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String getHelpTopic() {
|
||||
return "settings.github";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public JComponent createComponent() {
|
||||
if (mySettingsPane == null) {
|
||||
mySettingsPane = new GithubSettingsPanel();
|
||||
}
|
||||
return mySettingsPane.getPanel();
|
||||
}
|
||||
|
||||
public boolean isModified() {
|
||||
return mySettingsPane != null && mySettingsPane.isModified();
|
||||
}
|
||||
|
||||
public void apply() {
|
||||
if (mySettingsPane != null) {
|
||||
mySettingsPane.apply();
|
||||
}
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
if (mySettingsPane != null) {
|
||||
mySettingsPane.reset();
|
||||
}
|
||||
}
|
||||
|
||||
public void disposeUIResources() {
|
||||
mySettingsPane = null;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getId() {
|
||||
return getHelpTopic();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Configurable getConfigurable(Project project) {
|
||||
return this;
|
||||
}
|
||||
inner class GithubSettingsHolder internal constructor(val application: GithubSettings,
|
||||
val applicationAccounts: GithubAccountManager,
|
||||
val projectAccount: GithubProjectDefaultAccountHolder)
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.jetbrains.plugins.github.ui.GithubSettingsPanel">
|
||||
<grid id="27dc6" binding="myPane" layout-manager="GridLayoutManager" row-count="5" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||
<margin top="0" left="0" bottom="0" right="0"/>
|
||||
<constraints>
|
||||
<xy x="20" y="20" width="646" height="400"/>
|
||||
</constraints>
|
||||
<properties/>
|
||||
<clientProperties>
|
||||
<html.disable class="java.lang.Boolean" value="false"/>
|
||||
</clientProperties>
|
||||
<border type="none"/>
|
||||
<children>
|
||||
<nested-form id="abfc3" form-file="org/jetbrains/plugins/github/ui/GithubCredentialsPanel.form" binding="myCredentialsPanel" custom-create="true">
|
||||
<constraints>
|
||||
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
</nested-form>
|
||||
<component id="9abec" class="com.intellij.ui.TitledSeparator">
|
||||
<constraints>
|
||||
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties/>
|
||||
</component>
|
||||
<component id="e2613" class="com.intellij.ui.components.JBCheckBox" binding="myCloneUsingSshCheckBox" default-binding="true">
|
||||
<constraints>
|
||||
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties>
|
||||
<text value="Clone git repositories using ssh"/>
|
||||
</properties>
|
||||
</component>
|
||||
<grid id="d384a" layout-manager="GridLayoutManager" row-count="1" column-count="4" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||
<margin top="0" left="0" bottom="0" right="0"/>
|
||||
<constraints>
|
||||
<grid row="3" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties/>
|
||||
<clientProperties>
|
||||
<html.disable class="java.lang.Boolean" value="false"/>
|
||||
</clientProperties>
|
||||
<border type="none"/>
|
||||
<children>
|
||||
<component id="ae995" class="javax.swing.JLabel">
|
||||
<constraints>
|
||||
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties>
|
||||
<labelFor value="cd677"/>
|
||||
<text value="Connection timeout:"/>
|
||||
</properties>
|
||||
</component>
|
||||
<component id="cd677" class="javax.swing.JSpinner" binding="myTimeoutSpinner" custom-create="true">
|
||||
<constraints>
|
||||
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties/>
|
||||
</component>
|
||||
<component id="628fe" class="javax.swing.JLabel">
|
||||
<constraints>
|
||||
<grid row="0" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties>
|
||||
<text value="ms"/>
|
||||
</properties>
|
||||
</component>
|
||||
<hspacer id="f53f1">
|
||||
<constraints>
|
||||
<grid row="0" column="3" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
</hspacer>
|
||||
</children>
|
||||
</grid>
|
||||
<vspacer id="6ace">
|
||||
<constraints>
|
||||
<grid row="4" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
</vspacer>
|
||||
</children>
|
||||
</grid>
|
||||
</form>
|
||||
@@ -1,109 +1,96 @@
|
||||
/*
|
||||
* Copyright 2000-2016 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.jetbrains.plugins.github.ui;
|
||||
// Copyright 2000-2018 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
|
||||
|
||||
import com.intellij.openapi.project.ProjectManager;
|
||||
import com.intellij.openapi.util.Comparing;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.ui.components.JBCheckBox;
|
||||
import gnu.trove.Equality;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.plugins.github.util.GithubAuthData;
|
||||
import org.jetbrains.plugins.github.util.GithubSettings;
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.options.ConfigurableUi
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.ui.components.JBCheckBox
|
||||
import com.intellij.util.ui.JBDimension
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UI.PanelFactory.grid
|
||||
import com.intellij.util.ui.UI.PanelFactory.panel
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import org.jetbrains.plugins.github.authentication.accounts.GithubAccountInformationProvider
|
||||
import org.jetbrains.plugins.github.authentication.ui.GithubAccountsPanel
|
||||
import org.jetbrains.plugins.github.util.GithubSettings
|
||||
import java.awt.Component.LEFT_ALIGNMENT
|
||||
import java.awt.GridLayout
|
||||
import java.text.NumberFormat
|
||||
import javax.swing.*
|
||||
import javax.swing.text.NumberFormatter
|
||||
|
||||
import javax.swing.*;
|
||||
class GithubSettingsPanel(project: Project, accountInformationProvider: GithubAccountInformationProvider)
|
||||
: ConfigurableUi<GithubSettingsConfigurable.GithubSettingsHolder>, Disposable {
|
||||
private val accountsPanel = GithubAccountsPanel(project, accountInformationProvider)
|
||||
private val timeoutField = JFormattedTextField(NumberFormatter(NumberFormat.getIntegerInstance()).apply {
|
||||
minimum = 0
|
||||
maximum = 60
|
||||
}).apply {
|
||||
columns = 2
|
||||
UIUtil.fixFormattedField(this)
|
||||
}
|
||||
private val cloneUsingSshCheckBox = JBCheckBox("Clone git repositories using ssh")
|
||||
|
||||
public class GithubSettingsPanel {
|
||||
private final GithubSettings mySettings;
|
||||
|
||||
private JPanel myPane;
|
||||
private JSpinner myTimeoutSpinner;
|
||||
private JBCheckBox myCloneUsingSshCheckBox;
|
||||
private GithubCredentialsPanel myCredentialsPanel;
|
||||
|
||||
public GithubSettingsPanel() {
|
||||
mySettings = GithubSettings.getInstance();
|
||||
|
||||
reset();
|
||||
override fun reset(settings: GithubSettingsConfigurable.GithubSettingsHolder) {
|
||||
accountsPanel.setAccounts(settings.applicationAccounts.accounts, settings.projectAccount.account)
|
||||
accountsPanel.clearNewTokens()
|
||||
accountsPanel.loadExistingAccountsDetails()
|
||||
timeoutField.value = settings.application.getConnectionTimeoutSeconds()
|
||||
cloneUsingSshCheckBox.isSelected = settings.application.isCloneGitUsingSsh
|
||||
}
|
||||
|
||||
public JComponent getPanel() {
|
||||
return myPane;
|
||||
override fun isModified(settings: GithubSettingsConfigurable.GithubSettingsHolder): Boolean {
|
||||
return timeoutField.value != settings.application.getConnectionTimeoutSeconds() ||
|
||||
cloneUsingSshCheckBox.isSelected != settings.application.isCloneGitUsingSsh ||
|
||||
accountsPanel.isModified(settings.applicationAccounts.accounts, settings.projectAccount.account)
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public GithubAuthData getAuthData() {
|
||||
GithubAuthData authData = myCredentialsPanel.getAuthData();
|
||||
if (authData.getBasicAuth() != null && StringUtil.isEmptyOrSpaces(authData.getBasicAuth().getLogin()) ||
|
||||
authData.getTokenAuth() != null && StringUtil.isEmptyOrSpaces(authData.getTokenAuth().getToken())) {
|
||||
return GithubAuthData.createAnonymous(myCredentialsPanel.getHost());
|
||||
override fun apply(settings: GithubSettingsConfigurable.GithubSettingsHolder) {
|
||||
val (accountsTokenMap, defaultAccount) = accountsPanel.getAccounts()
|
||||
settings.applicationAccounts.accounts = accountsTokenMap.keys
|
||||
accountsTokenMap.filterValues { it != null }.forEach(settings.applicationAccounts::updateAccountToken)
|
||||
settings.projectAccount.account = defaultAccount
|
||||
accountsPanel.clearNewTokens()
|
||||
settings.application.setConnectionTimeoutSeconds(timeoutField.value as Int)
|
||||
settings.application.isCloneGitUsingSsh = cloneUsingSshCheckBox.isSelected
|
||||
}
|
||||
|
||||
private fun GithubSettings.getConnectionTimeoutSeconds(): Int {
|
||||
return connectionTimeout / 1000
|
||||
}
|
||||
|
||||
private fun GithubSettings.setConnectionTimeoutSeconds(timeout: Int) {
|
||||
connectionTimeout = timeout * 1000
|
||||
}
|
||||
|
||||
override fun getComponent(): JComponent {
|
||||
val timeoutPanel = JPanel().apply {
|
||||
layout = BoxLayout(this, BoxLayout.X_AXIS)
|
||||
add(JLabel("Connection timeout:"))
|
||||
add(Box.createRigidArea(JBDimension(UIUtil.DEFAULT_HGAP, 0)))
|
||||
add(timeoutField)
|
||||
add(Box.createRigidArea(JBDimension(UIUtil.DEFAULT_HGAP, 0)))
|
||||
add(JLabel("seconds"))
|
||||
alignmentX = LEFT_ALIGNMENT
|
||||
}
|
||||
|
||||
return authData;
|
||||
}
|
||||
val settingsPanel = grid()
|
||||
.add(panel(cloneUsingSshCheckBox.apply { alignmentX = LEFT_ALIGNMENT }))
|
||||
.add(panel(timeoutPanel).resizeX(false))
|
||||
.createPanel()
|
||||
.apply {
|
||||
border = JBUI.Borders.empty(UIUtil.DEFAULT_VGAP, UIUtil.DEFAULT_HGAP)
|
||||
}
|
||||
|
||||
public void setConnectionTimeout(int timeout) {
|
||||
myTimeoutSpinner.setValue(Integer.valueOf(timeout));
|
||||
}
|
||||
|
||||
public int getConnectionTimeout() {
|
||||
return ((SpinnerNumberModel)myTimeoutSpinner.getModel()).getNumber().intValue();
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
myCredentialsPanel.setAuthData(mySettings.getAuthData());
|
||||
|
||||
setConnectionTimeout(mySettings.getConnectionTimeout());
|
||||
myCloneUsingSshCheckBox.setSelected(mySettings.isCloneGitUsingSsh());
|
||||
}
|
||||
|
||||
public void apply() {
|
||||
if (!equal(mySettings.getAuthData(), getAuthData())) {
|
||||
mySettings.setAuthData(getAuthData(), true);
|
||||
return JPanel().apply {
|
||||
layout = GridLayout(2, 1)
|
||||
add(accountsPanel)
|
||||
add(settingsPanel)
|
||||
}
|
||||
mySettings.setConnectionTimeout(getConnectionTimeout());
|
||||
mySettings.setCloneGitUsingSsh(myCloneUsingSshCheckBox.isSelected());
|
||||
}
|
||||
|
||||
public boolean isModified() {
|
||||
return !equal(mySettings.getAuthData(), getAuthData()) ||
|
||||
!Comparing.equal(mySettings.getConnectionTimeout(), getConnectionTimeout()) ||
|
||||
!Comparing.equal(mySettings.isCloneGitUsingSsh(), myCloneUsingSshCheckBox.isSelected());
|
||||
}
|
||||
|
||||
private void createUIComponents() {
|
||||
myCredentialsPanel = new GithubCredentialsPanel(ProjectManager.getInstance().getDefaultProject());
|
||||
myTimeoutSpinner = new JSpinner(new SpinnerNumberModel(5000, 0, 60000, 500));
|
||||
}
|
||||
|
||||
private static boolean equal(@NotNull GithubAuthData data1, @NotNull GithubAuthData data2) {
|
||||
return Comparing.equal(data1.getHost(), data2.getHost()) &&
|
||||
Comparing.equal(data1.getAuthType(), data2.getAuthType()) &&
|
||||
equal(data1.getBasicAuth(), data2.getBasicAuth(),
|
||||
(auth1, auth2) -> Comparing.equal(auth1.getLogin(), auth2.getLogin()) &&
|
||||
Comparing.equal(auth1.getPassword(), auth2.getPassword())) &&
|
||||
equal(data1.getTokenAuth(), data2.getTokenAuth(),
|
||||
(auth1, auth2) -> Comparing.equal(auth1.getToken(), auth2.getToken()));
|
||||
}
|
||||
|
||||
private static <T> boolean equal(@Nullable T o1, @Nullable T o2, @NotNull Equality<T> notNullEquality) {
|
||||
if (o1 == o2) return true;
|
||||
if (o1 == null) return false;
|
||||
if (o2 == null) return false;
|
||||
return notNullEquality.equals(o1, o2);
|
||||
override fun dispose() {
|
||||
Disposer.dispose(accountsPanel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2000-2018 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.openapi.ui.ValidationInfo
|
||||
import javax.swing.JTextField
|
||||
|
||||
object DialogValidationUtils {
|
||||
/**
|
||||
* Returns [ValidationInfo] with [message] if [textField] is blank
|
||||
*/
|
||||
fun notBlank(textField: JTextField, message: String): ValidationInfo? {
|
||||
return if (textField.text.isNullOrBlank()) ValidationInfo(message, textField) else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Chains the [validators] so that if one of them returns non-null [ValidationInfo] the rest of them are not checked
|
||||
*/
|
||||
fun chain(vararg validators: Validator): Validator = { validators.asSequence().mapNotNull { it() }.firstOrNull() }
|
||||
}
|
||||
|
||||
typealias Validator = () -> ValidationInfo?
|
||||
@@ -72,6 +72,7 @@ public class GithubUtil {
|
||||
|
||||
public static final Logger LOG = Logger.getInstance("github");
|
||||
public static final String SERVICE_DISPLAY_NAME = "GitHub";
|
||||
public static final String DEFAULT_TOKEN_NOTE = "IntelliJ Plugin";
|
||||
|
||||
// TODO: Consider sharing of GithubAuthData between actions (as member of GithubSettings)
|
||||
public static <T> T runTask(@NotNull Project project,
|
||||
|
||||
Reference in New Issue
Block a user