[github] Simplify pull requests list components structure

This commit is contained in:
Ivan Semenov
2018-10-02 18:45:38 +03:00
parent 71cc00e245
commit a3d4eddacf
6 changed files with 165 additions and 163 deletions
@@ -9,7 +9,6 @@ import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.ui.OnePixelSplitter
import com.intellij.ui.components.panels.Wrapper
import git4idea.commands.Git
import git4idea.repo.GitRemote
import git4idea.repo.GitRepository
@@ -19,9 +18,14 @@ import org.jetbrains.plugins.github.authentication.accounts.GithubAccount
import org.jetbrains.plugins.github.pullrequest.action.GithubPullRequestKeys
import org.jetbrains.plugins.github.pullrequest.avatars.CachingGithubAvatarIconsProvider
import org.jetbrains.plugins.github.pullrequest.config.GithubPullRequestsUISettings
import org.jetbrains.plugins.github.pullrequest.config.GithubPullRequestsUISettings.SettingsChangedListener
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsDataLoader
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsLoader
import org.jetbrains.plugins.github.pullrequest.ui.*
import org.jetbrains.plugins.github.pullrequest.ui.GithubPullRequestChangesComponent
import org.jetbrains.plugins.github.pullrequest.ui.GithubPullRequestDetailsComponent
import org.jetbrains.plugins.github.pullrequest.ui.GithubPullRequestPreviewComponent
import org.jetbrains.plugins.github.pullrequest.ui.GithubPullRequestsListComponent
import org.jetbrains.plugins.github.pullrequest.ui.GithubPullRequestsListSelectionModel.SelectionChangedListener
import org.jetbrains.plugins.github.util.CachingGithubUserAvatarLoader
import org.jetbrains.plugins.github.util.GithubImageResizer
import javax.swing.JComponent
@@ -40,69 +44,76 @@ class GithubPullRequestsComponentFactory(private val project: Project,
repository: GitRepository, remote: GitRemote,
repoDetails: GithubRepoDetailed,
account: GithubAccount): JComponent? {
val avatarIconsProviderFactory = CachingGithubAvatarIconsProvider.Factory(avatarLoader, imageResizer, requestExecutor)
return GithubPullRequestsComponent(requestExecutor, avatarIconsProviderFactory, repository, remote, repoDetails, account)
}
val listLoader = GithubPullRequestsLoader(progressManager, requestExecutor,
account.server, repoDetails.fullPath)
val selectionModel = GithubPullRequestsListSelectionModel()
val list = GithubPullRequestsListComponent(project, actionManager, autoPopupController,
selectionModel, listLoader,
CachingGithubAvatarIconsProvider.Factory(avatarLoader, imageResizer, requestExecutor))
requestExecutor.addListener(list) { list.refresh() }
private inner class GithubPullRequestsComponent(requestExecutor: GithubApiRequestExecutor,
avatarIconsProviderFactory: CachingGithubAvatarIconsProvider.Factory,
private val repository: GitRepository, private val remote: GitRemote,
private val repoDetails: GithubRepoDetailed,
private val account: GithubAccount)
: OnePixelSplitter("Github.PullRequests.Component", 0.6f), Disposable, DataProvider {
val dataLoader = GithubPullRequestsDataLoader(project, progressManager, git, requestExecutor, repository, remote)
private val dataLoader = GithubPullRequestsDataLoader(project, progressManager, git, requestExecutor, repository, remote)
val changes = GithubPullRequestChangesComponent(project, selectionModel, dataLoader, actionManager)
val details = GithubPullRequestDetailsComponent(project, selectionModel, dataLoader)
private val changes = GithubPullRequestChangesComponent(project, actionManager).apply {
diffAction.registerCustomShortcutSet(this@GithubPullRequestsComponent, this@GithubPullRequestsComponent)
}
private val details = GithubPullRequestDetailsComponent(project)
private val preview = GithubPullRequestPreviewComponent(changes, details).apply {
detailsVisible = uiSettings.showDetails
}
val preview = GithubPullRequestPreviewComponent(uiSettings, changes, details)
list.setToolbarHeightReferent(preview.toolbarComponent)
private val listLoader = GithubPullRequestsLoader(progressManager, requestExecutor, account.server, repoDetails.fullPath)
private val list = GithubPullRequestsListComponent(project, actionManager, autoPopupController,
listLoader,
avatarIconsProviderFactory).apply {
requestExecutor.addListener(this) { this.refresh() }
setToolbarHeightReferent(preview.toolbarComponent)
}
val splitter = OnePixelSplitter("Github.PullRequests.Component", 0.6f)
splitter.firstComponent = list
splitter.secondComponent = preview
// disposed by content manager when tab is closed
val wrapper = WrappingComponent(splitter,
repository, remote, repoDetails, account,
list,selectionModel, dataLoader)
Disposer.register(wrapper, Disposable {
init {
firstComponent = list
secondComponent = preview
isFocusCycleRoot = true
uiSettings.addChangesListener(object : SettingsChangedListener {
override fun settingsChanged() {
preview.detailsVisible = uiSettings.showDetails
}
}, preview)
list.selectionModel.addChangesListener(object : SelectionChangedListener {
override fun selectionChanged() {
val dataProvider = list.selectionModel.current?.let(dataLoader::getDataProvider)
preview.setPreviewDataProvider(dataProvider)
}
}, preview)
}
override fun getData(dataId: String): Any? {
return when {
GithubPullRequestKeys.REPOSITORY.`is`(dataId) -> repository
GithubPullRequestKeys.REMOTE.`is`(dataId) -> remote
GithubPullRequestKeys.REPO_DETAILS.`is`(dataId) -> repoDetails
GithubPullRequestKeys.SERVER_PATH.`is`(dataId) -> account.server
GithubPullRequestKeys.PULL_REQUESTS_LIST_COMPONENT.`is`(dataId) -> list
GithubPullRequestKeys.SELECTED_PULL_REQUEST_DATA_PROVIDER.`is`(dataId) ->
list.selectionModel.current?.let(dataLoader::getDataProvider)
else -> null
}
}
override fun dispose() {
Disposer.dispose(list)
Disposer.dispose(preview)
Disposer.dispose(changes)
Disposer.dispose(details)
Disposer.dispose(listLoader)
Disposer.dispose(dataLoader)
})
changes.diffAction.registerCustomShortcutSet(wrapper, wrapper)
return wrapper
}
companion object {
private class WrappingComponent(wrapped: JComponent,
private val repository: GitRepository,
private val remote: GitRemote,
private val repoDetails: GithubRepoDetailed,
private val account: GithubAccount,
private val list: GithubPullRequestsListComponent,
private val selectionModel: GithubPullRequestsListSelectionModel,
private val dataLoader: GithubPullRequestsDataLoader)
: Wrapper(wrapped), Disposable, DataProvider {
init {
isFocusCycleRoot = true
}
override fun getData(dataId: String): Any? {
return when {
GithubPullRequestKeys.REPOSITORY.`is`(dataId) -> repository
GithubPullRequestKeys.REMOTE.`is`(dataId) -> remote
GithubPullRequestKeys.REPO_DETAILS.`is`(dataId) -> repoDetails
GithubPullRequestKeys.SERVER_PATH.`is`(dataId) -> account.server
GithubPullRequestKeys.PULL_REQUESTS_LIST_COMPONENT.`is`(dataId) -> list
GithubPullRequestKeys.SELECTED_PULL_REQUEST_DATA_PROVIDER.`is`(dataId) -> selectionModel.current?.let(dataLoader::getDataProvider)
else -> null
}
}
override fun dispose() {}
}
}
}
@@ -0,0 +1,44 @@
// 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.pullrequest.ui
import com.intellij.ui.components.panels.Wrapper
import org.jetbrains.annotations.CalledInAwt
import org.jetbrains.plugins.github.util.GithubAsyncUtil
import org.jetbrains.plugins.github.util.handleOnEdt
import java.util.concurrent.CompletableFuture
abstract class GithubDataLoadingComponent<T> : Wrapper() {
private var updateFuture: CompletableFuture<Unit>? = null
/**
* This works because [handleOnEdt] basically forms a EDT-synchronized section and result/exception is acquired from [dataRequest] on EDT
*
* In pseudocode:
* when (dataRequest.isDone) { runOnEdt { handler(getResult(), getException()) } }
*/
@CalledInAwt
fun loadAndShow(dataRequest: CompletableFuture<T>?) {
updateFuture?.cancel(true)
reset()
if (dataRequest == null) {
updateFuture = null
setBusy(false)
return
}
setBusy(true)
updateFuture = dataRequest.handleOnEdt { result, error ->
when {
error != null && !GithubAsyncUtil.isCancellation(error) -> handleError(error)
result != null -> handleResult(result)
}
setBusy(false)
}
}
protected abstract fun reset()
protected abstract fun handleResult(result: T)
protected abstract fun handleError(error: Throwable)
protected abstract fun setBusy(busy: Boolean)
}
@@ -15,73 +15,53 @@ import com.intellij.ui.IdeBorderFactory
import com.intellij.ui.SideBorder
import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.components.JBLoadingPanel
import com.intellij.ui.components.panels.Wrapper
import com.intellij.util.ui.ComponentWithEmptyText
import org.jetbrains.plugins.github.api.data.GithubSearchedIssue
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsDataLoader
import org.jetbrains.plugins.github.util.GithubAsyncUtil
import org.jetbrains.plugins.github.util.handleOnEdt
import java.awt.BorderLayout
import java.util.concurrent.CompletableFuture
import javax.swing.JComponent
import javax.swing.border.Border
import kotlin.properties.Delegates
class GithubPullRequestChangesComponent(project: Project,
private val selectionModel: GithubPullRequestsListSelectionModel,
private val dataLoader: GithubPullRequestsDataLoader,
actionManager: ActionManager)
: Wrapper(), Disposable, GithubPullRequestsListSelectionModel.SelectionChangedListener {
class GithubPullRequestChangesComponent(project: Project, actionManager: ActionManager)
: GithubDataLoadingComponent<List<Change>>(), Disposable {
private val changesBrowser = PullRequestChangesBrowserWithError(project, actionManager)
private val loadingPanel = JBLoadingPanel(BorderLayout(), this, ProgressWindow.DEFAULT_PROGRESS_DIALOG_POSTPONE_TIME_MILLIS)
val toolbarComponent: JComponent = changesBrowser.toolbar.component
val diffAction = changesBrowser.diffAction
private val changesLoadingPanel = JBLoadingPanel(BorderLayout(), this,
ProgressWindow.DEFAULT_PROGRESS_DIALOG_POSTPONE_TIME_MILLIS)
private var updateFuture: CompletableFuture<Unit>? = null
init {
selectionModel.addChangesListener(this, this)
changesLoadingPanel.add(changesBrowser, BorderLayout.CENTER)
setContent(changesLoadingPanel)
loadingPanel.add(changesBrowser, BorderLayout.CENTER)
changesBrowser.emptyText.text = DEFAULT_EMPTY_TEXT
setContent(loadingPanel)
}
override fun selectionChanged() {
reset()
updateFuture = updateChanges(selectionModel.current)
}
private fun updateChanges(item: GithubSearchedIssue?) =
item?.let { selection ->
changesBrowser.emptyText.clear()
changesLoadingPanel.startLoading()
dataLoader.getDataProvider(selection).changesRequest
.handleOnEdt { changes, error ->
when {
error != null && !GithubAsyncUtil.isCancellation(error) -> {
changesBrowser.emptyText
.appendText("Cannot load changes", SimpleTextAttributes.ERROR_ATTRIBUTES)
.appendSecondaryText(error.message ?: "Unknown error", SimpleTextAttributes.ERROR_ATTRIBUTES, null)
}
changes != null -> {
changesBrowser.emptyText.text = "Pull request does not contain any changes"
changesBrowser.changes = changes
}
}
changesLoadingPanel.stopLoading()
}
}
private fun reset() {
updateFuture?.cancel(true)
override fun reset() {
changesBrowser.emptyText.text = DEFAULT_EMPTY_TEXT
changesBrowser.changes = emptyList()
}
override fun handleResult(result: List<Change>) {
changesBrowser.emptyText.text = "Pull request does not contain any changes"
changesBrowser.changes = result
}
override fun handleError(error: Throwable) {
changesBrowser.emptyText
.clear()
.appendText("Cannot load changes", SimpleTextAttributes.ERROR_ATTRIBUTES)
.appendSecondaryText(error.message ?: "Unknown error", SimpleTextAttributes.ERROR_ATTRIBUTES, null)
}
override fun setBusy(busy: Boolean) {
if (busy) {
changesBrowser.emptyText.clear()
loadingPanel.startLoading()
}
else {
loadingPanel.stopLoading()
}
}
override fun dispose() {}
companion object {
@@ -6,62 +6,35 @@ import com.intellij.openapi.progress.util.ProgressWindow
import com.intellij.openapi.project.Project
import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.components.JBLoadingPanel
import com.intellij.ui.components.panels.Wrapper
import org.jetbrains.plugins.github.api.data.GithubSearchedIssue
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsDataLoader
import org.jetbrains.plugins.github.util.GithubAsyncUtil
import org.jetbrains.plugins.github.util.handleOnEdt
import org.jetbrains.plugins.github.api.data.GithubPullRequestDetailedWithHtml
import java.awt.BorderLayout
import java.util.concurrent.CompletableFuture
class GithubPullRequestDetailsComponent(project: Project,
private val selectionModel: GithubPullRequestsListSelectionModel,
private val dataLoader: GithubPullRequestsDataLoader)
: Wrapper(), Disposable, GithubPullRequestsListSelectionModel.SelectionChangedListener {
class GithubPullRequestDetailsComponent(project: Project) : GithubDataLoadingComponent<GithubPullRequestDetailedWithHtml>(), Disposable {
private val detailsPanel = GithubPullRequestDetailsPanel(project)
private val loadingPanel = JBLoadingPanel(BorderLayout(), this, ProgressWindow.DEFAULT_PROGRESS_DIALOG_POSTPONE_TIME_MILLIS)
private var updateFuture: CompletableFuture<Unit>? = null
init {
selectionModel.addChangesListener(this, this)
loadingPanel.add(detailsPanel)
setContent(loadingPanel)
}
override fun selectionChanged() {
reset()
updateFuture = updateDetails(selectionModel.current)
}
private fun updateDetails(item: GithubSearchedIssue?) =
item?.let { selection ->
loadingPanel.startLoading()
dataLoader.getDataProvider(selection).detailsRequest
.handleOnEdt { details, error ->
when {
error != null && !GithubAsyncUtil.isCancellation(error) -> {
detailsPanel.emptyText
.appendText("Cannot load details", SimpleTextAttributes.ERROR_ATTRIBUTES)
.appendSecondaryText(error.message ?: "Unknown error", SimpleTextAttributes.ERROR_ATTRIBUTES, null)
}
details != null -> {
detailsPanel.details = details
}
}
loadingPanel.stopLoading()
}
}
private fun reset() {
updateFuture?.cancel(true)
override fun reset() {
detailsPanel.emptyText.clear()
detailsPanel.details = null
}
override fun handleResult(result: GithubPullRequestDetailedWithHtml) {
detailsPanel.details = result
}
override fun handleError(error: Throwable) {
detailsPanel.emptyText
.clear()
.appendText("Cannot load details", SimpleTextAttributes.ERROR_ATTRIBUTES)
.appendSecondaryText(error.message ?: "Unknown error", SimpleTextAttributes.ERROR_ATTRIBUTES, null)
}
override fun setBusy(busy: Boolean) = if (busy) loadingPanel.startLoading() else loadingPanel.stopLoading()
override fun dispose() {}
}
@@ -2,33 +2,27 @@
package org.jetbrains.plugins.github.pullrequest.ui
import com.intellij.openapi.Disposable
import com.intellij.openapi.util.Disposer
import com.intellij.ui.OnePixelSplitter
import org.jetbrains.plugins.github.pullrequest.config.GithubPullRequestsUISettings
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsDataLoader
class GithubPullRequestPreviewComponent(private val uiSettings: GithubPullRequestsUISettings,
changes: GithubPullRequestChangesComponent,
class GithubPullRequestPreviewComponent(private val changes: GithubPullRequestChangesComponent,
private val details: GithubPullRequestDetailsComponent)
: OnePixelSplitter(true, "Github.PullRequest.Preview.Component", 0.6f),
Disposable, GithubPullRequestsUISettings.SettingsChangedListener {
: OnePixelSplitter(true, "Github.PullRequest.Preview.Component", 0.6f), Disposable {
val toolbarComponent = changes.toolbarComponent
var detailsVisible: Boolean
get() = secondComponent != null
set(value) {
secondComponent = if (value) details else null
}
init {
Disposer.register(this, changes)
Disposer.register(this, details)
firstComponent = changes
uiSettings.addChangesListener(this, this)
updateDetails()
}
override fun settingsChanged() {
updateDetails()
}
private fun updateDetails() {
secondComponent = if (uiSettings.showDetails) details else null
fun setPreviewDataProvider(provider: GithubPullRequestsDataLoader.DataProvider?) {
changes.loadAndShow(provider?.changesRequest)
details.loadAndShow(provider?.detailsRequest)
}
override fun dispose() {}
@@ -34,11 +34,11 @@ import javax.swing.event.ListSelectionEvent
class GithubPullRequestsListComponent internal constructor(project: Project,
actionManager: ActionManager,
autoPopupController: AutoPopupController,
private val selectionModel: GithubPullRequestsListSelectionModel,
private val loader: GithubPullRequestsLoader,
avatarIconsProviderFactory: CachingGithubAvatarIconsProvider.Factory)
: BorderLayoutPanel(), Disposable, DataProvider {
val selectionModel = GithubPullRequestsListSelectionModel()
private val listModel = CollectionListModel<GithubSearchedIssue>()
private val list = GithubPullRequestsList(avatarIconsProviderFactory, listModel)
private val scrollPane = ScrollPaneFactory.createScrollPane(list,