IDEA-308635 respect dialog checkboxes

GitOrigin-RevId: 278a64b223fbcb1ba40f1b4b130cd46904d70e8a
This commit is contained in:
Sergey Pak
2023-07-06 23:55:14 +00:00
committed by intellij-monorepo-bot
parent bab4206b8e
commit da992dce67
21 changed files with 490 additions and 209 deletions

View File

@@ -2,7 +2,6 @@ package com.intellij.settingsSync
import com.intellij.openapi.util.NlsSafe
import org.jetbrains.annotations.ApiStatus.Internal
import java.util.*
@Internal
@@ -18,7 +17,8 @@ sealed class SyncSettingsEvent {
sealed class ExclusiveEvent : SyncSettingsEvent()
class IdeChange(snapshot: SettingsSnapshot) : EventWithSnapshot(snapshot)
class CloudChange(snapshot: SettingsSnapshot, val serverVersionId: String?) : EventWithSnapshot(snapshot)
class CloudChange(snapshot: SettingsSnapshot, val serverVersionId: String?, val syncSettings: SettingsSyncState? = null)
: EventWithSnapshot(snapshot)
object MustPushRequest : StandardEvent()
object LogCurrentSettings : StandardEvent()

View File

@@ -2,10 +2,13 @@ package com.intellij.settingsSync
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.application.PathManager
import com.intellij.openapi.components.SettingsCategory
import com.intellij.openapi.components.service
import com.intellij.openapi.util.BuildNumber
import com.intellij.openapi.util.JDOMUtil
import com.intellij.settingsSync.plugins.SettingsSyncPluginsState
import com.intellij.util.SystemProperties
import com.intellij.util.xmlb.XmlSerializer
import org.jetbrains.annotations.ApiStatus
import java.time.Instant
import java.util.*
@@ -53,6 +56,29 @@ data class SettingsSnapshot(val metaInfo: MetaInfo,
fun isDeleted(): Boolean {
return metaInfo.isDeleted
}
fun getState(): SettingsSyncState {
val fileState = fileStates.firstOrNull {
it.file == ("${PathManager.OPTIONS_DIRECTORY}/${SettingsSyncSettings.FILE_SPEC}")
} ?: return SettingsSyncStateHolder()
if (fileState !is FileState.Modified) {
return SettingsSyncStateHolder()
}
try {
val componentElement = JDOMUtil.load(fileState.content).getChildren("component")
.firstOrNull { it.getAttributeValue("name") == SettingsSyncSettings.COMPONENT_NAME }
?: return SettingsSyncStateHolder()
val state = XmlSerializer.deserialize(componentElement, SettingsSyncSettings.State::class.java)
return SettingsSyncStateHolder(
state
)
}
catch (ex: Throwable) {
SettingsSyncSettings.LOG.error("Unable to deserialize content of ${SettingsSyncSettings.FILE_SPEC} into object", ex)
return SettingsSyncStateHolder()
}
}
}
@ApiStatus.Internal

View File

@@ -102,7 +102,7 @@ class SettingsSyncBridge(parentDisposable: Disposable,
settingsLog.advanceMaster() // merge (preserve) 'ide' changes made by logging existing settings
val masterPosition = settingsLog.forceWriteToMaster(cloudEvent.snapshot, "Remote changes to initialize settings by data from cloud")
pushToIde(settingsLog.collectCurrentSnapshot(), masterPosition)
pushToIde(settingsLog.collectCurrentSnapshot(), masterPosition, cloudEvent.syncSettings)
// normally we set cloud position only after successful push to cloud, but in this case we already take all settings from the cloud,
// so no push is needed, and we know the cloud settings state.
@@ -127,7 +127,7 @@ class SettingsSyncBridge(parentDisposable: Disposable,
SettingsSyncLocalSettings.getInstance().knownAndAppliedServerId = updateResult.serverVersionId
SettingsSyncSettings.getInstance().syncEnabled = true
pushToIde(settingsLog.collectCurrentSnapshot(), masterPosition)
pushToIde(settingsLog.collectCurrentSnapshot(), masterPosition, null)
}
is UpdateResult.FileDeletedFromServer -> {
SettingsSyncSettings.getInstance().syncEnabled = false
@@ -144,7 +144,7 @@ class SettingsSyncBridge(parentDisposable: Disposable,
settingsLog.setCloudPosition(masterPosition)
SettingsSyncSettings.getInstance().syncEnabled = true
pushToIde(settingsLog.collectCurrentSnapshot(), masterPosition)
pushToIde(settingsLog.collectCurrentSnapshot(), masterPosition, null)
migration.migrateCategoriesSyncStatus(appConfigPath, SettingsSyncSettings.getInstance())
saveIdeSettings()
}
@@ -333,7 +333,7 @@ class SettingsSyncBridge(parentDisposable: Disposable,
}
if (newIdePosition != masterPosition) { // master has advanced further that ide => the ide needs to be updated
pushToIde(settingsLog.collectCurrentSnapshot(), masterPosition)
pushToIde(settingsLog.collectCurrentSnapshot(), masterPosition, null)
}
if (newCloudPosition != masterPosition || pushRequestMode == MUST_PUSH || pushRequestMode == FORCE_PUSH) {
@@ -399,8 +399,8 @@ class SettingsSyncBridge(parentDisposable: Disposable,
}
}
private fun pushToIde(settingsSnapshot: SettingsSnapshot, targetPosition: SettingsLog.Position) {
ideMediator.applyToIde(settingsSnapshot)
private fun pushToIde(settingsSnapshot: SettingsSnapshot, targetPosition: SettingsLog.Position, syncSettings: SettingsSyncState?) {
ideMediator.applyToIde(settingsSnapshot, syncSettings)
settingsLog.setIdePosition(targetPosition)
LOG.info("Applied settings to the IDE.")
}

View File

@@ -11,7 +11,10 @@ import java.nio.file.Path
@ApiStatus.Internal
interface SettingsSyncIdeMediator {
fun applyToIde(snapshot: SettingsSnapshot)
/**
* @param settings if not null, SettingsSync settings will be taken from this object rather than snapshot
*/
fun applyToIde(snapshot: SettingsSnapshot, settings: SettingsSyncState?)
fun activateStreamProvider()

View File

@@ -22,6 +22,7 @@ import com.intellij.openapi.util.registry.Registry
import com.intellij.settingsSync.SettingsSnapshot.MetaInfo
import com.intellij.settingsSync.plugins.SettingsSyncPluginManager
import com.intellij.util.io.*
import org.jdom.Element
import java.io.InputStream
import java.nio.file.FileVisitResult
import java.nio.file.Files
@@ -64,11 +65,17 @@ internal class SettingsSyncIdeMediatorImpl(private val componentStore: Component
return roamingType != RoamingType.DISABLED
}
override fun applyToIde(snapshot: SettingsSnapshot) {
override fun applyToIde(snapshot: SettingsSnapshot, settings: SettingsSyncState?) {
// 1. update SettingsSyncSettings first to apply changes in categories
val settingsSyncFileState = snapshot.fileStates.find { it.file == "$OPTIONS_DIRECTORY/${SettingsSyncSettings.FILE_SPEC}" }
if (settingsSyncFileState != null) {
writeStatesToAppConfig(listOf(settingsSyncFileState))
if (settings != null) {
LOG.info("applying sync settings from SettingsSyncState")
SettingsSyncSettings.getInstance().applyFromState(settings)
}
else {
if (settingsSyncFileState != null) {
writeStatesToAppConfig(listOf(settingsSyncFileState))
}
}
// 2. update plugins
@@ -283,10 +290,9 @@ internal class SettingsSyncIdeMediatorImpl(private val componentStore: Component
invokeAndWaitIfNeeded {
reloadComponents(changedFileSpecs, deletedFileSpecs)
if (Registry.getInstance().isRestartNeeded){
if (Registry.getInstance().isRestartNeeded) {
SettingsSyncEvents.getInstance().fireRestartRequired("registry", SettingsSyncBundle.message("sync.registry.update.message"))
}
}
}

View File

@@ -2,34 +2,41 @@ package com.intellij.settingsSync
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.*
import com.intellij.openapi.diagnostic.logger
import com.intellij.settingsSync.SettingsSyncSettings.Companion.COMPONENT_NAME
import com.intellij.settingsSync.SettingsSyncSettings.Companion.FILE_SPEC
import com.intellij.util.xmlb.annotations.Property
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.TestOnly
import java.util.*
import kotlin.collections.ArrayList
@State(name = "SettingsSyncSettings", storages = [Storage(FILE_SPEC)])
@State(name = COMPONENT_NAME, storages = [Storage(FILE_SPEC)])
@ApiStatus.Internal
class SettingsSyncSettings :
SimplePersistentStateComponent<SettingsSyncSettings.SettingsSyncSettingsState>(SettingsSyncSettingsState())
{
class SettingsSyncSettings : SettingsSyncState, SerializablePersistentStateComponent<SettingsSyncSettings.State>(State()) {
companion object {
fun getInstance() = ApplicationManager.getApplication().getService(SettingsSyncSettings::class.java)
fun getInstance(): SettingsSyncSettings = ApplicationManager.getApplication().getService(SettingsSyncSettings::class.java)
val LOG = logger<SettingsSyncSettings>()
const val FILE_SPEC = "settingsSync.xml"
const val COMPONENT_NAME = "SettingsSyncSettings"
}
var migrationFromOldStorageChecked: Boolean
override var migrationFromOldStorageChecked: Boolean
get() = state.migrationFromOldStorageChecked
set(value) {
state.migrationFromOldStorageChecked = value
updateState {
it.withMigrationFromOldStorageChecked(value)
}
}
var syncEnabled
override var syncEnabled
get() = state.syncEnabled
set(value) {
state.syncEnabled = value
updateState {
it.withSyncEnabled(value)
}
fireSettingsStateChanged(value)
}
@@ -37,65 +44,145 @@ class SettingsSyncSettings :
SettingsSyncEvents.getInstance().fireEnabledStateChanged(syncEnabled)
}
fun isCategoryEnabled(category: SettingsCategory) = !state.disabledCategories.contains(category)
override fun isCategoryEnabled(category: SettingsCategory) = state.isCategoryEnabled(category)
fun setCategoryEnabled(category: SettingsCategory, isEnabled: Boolean) {
if (isEnabled) {
state.disabledCategories.remove(category)
}
else {
if (!state.disabledCategories.contains(category)) {
state.disabledCategories.add(category)
state.disabledCategories.sort()
}
override fun setCategoryEnabled(category: SettingsCategory, isEnabled: Boolean) {
updateState {
it.withCategoryEnabled(category, isEnabled)
}
}
fun isSubcategoryEnabled(category: SettingsCategory, subcategoryId: String): Boolean {
val disabled = state.disabledSubcategories[category]
return disabled == null || !disabled.contains(subcategoryId)
override fun isSubcategoryEnabled(category: SettingsCategory, subcategoryId: String) = state.isSubcategoryEnabled(category, subcategoryId)
override fun setSubcategoryEnabled(category: SettingsCategory, subcategoryId: String, isEnabled: Boolean) {
updateState {
it.withSubcategoryEnabled(category, subcategoryId, isEnabled)
}
}
fun setSubcategoryEnabled(category: SettingsCategory, subcategoryId: String, isEnabled: Boolean) {
val disabledList = state.disabledSubcategories[category]
if (isEnabled) {
if (disabledList != null) {
disabledList.remove(subcategoryId)
if (disabledList.isEmpty()) {
state.disabledSubcategories.remove(category)
}
}
override val disabledCategories: List<SettingsCategory>
get() = state.disabledCategories
override val disabledSubcategories: Map<SettingsCategory, List<String>>
get() = state.disabledSubcategories
fun applyFromState(state: SettingsSyncState) {
updateState {
State(state.disabledCategories, state.disabledSubcategories, state.migrationFromOldStorageChecked, state.syncEnabled)
}
else {
if (disabledList == null) {
val newList = ArrayList<String>()
newList.add(subcategoryId)
state.disabledSubcategories.put(category, newList)
}
data class State(@JvmField val disabledCategories: List<SettingsCategory> = emptyList(),
@JvmField val disabledSubcategories: Map<SettingsCategory, List<String>> = emptyMap(),
@JvmField @field:Property val migrationFromOldStorageChecked: Boolean = false,
@JvmField @field:Property val syncEnabled: Boolean = false) {
fun withSyncEnabled(enabled: Boolean): State {
return State(disabledCategories, disabledSubcategories, migrationFromOldStorageChecked, enabled)
}
fun withMigrationFromOldStorageChecked(checked: Boolean): State {
return State(disabledCategories, disabledSubcategories, checked, syncEnabled)
}
private fun withDisabledCategories(newCategories: List<SettingsCategory>): State {
return State(newCategories, disabledSubcategories, migrationFromOldStorageChecked, syncEnabled)
}
fun withCategoryEnabled(category: SettingsCategory, isEnabled: Boolean): State {
val newCategories = ArrayList<SettingsCategory>(disabledCategories)
if (isEnabled) {
newCategories -= category
}
else {
if (!disabledList.contains(subcategoryId)) {
disabledList.add(subcategoryId)
Collections.sort(disabledList)
if (!newCategories.contains(category)) newCategories += category
}
newCategories.sort()
return withDisabledCategories(newCategories)
}
private fun withDisabledSubcategories(newSubcategoriesMap: Map<SettingsCategory, List<String>>): State {
return State(disabledCategories, newSubcategoriesMap, migrationFromOldStorageChecked, syncEnabled)
}
fun withSubcategoryEnabled(category: SettingsCategory, subcategoryId: String, isEnabled: Boolean): State {
val newSubcategoriesMap = HashMap(disabledSubcategories)
val subcategoriesList = newSubcategoriesMap[category]
if (isEnabled) {
if (subcategoriesList != null) {
val newSubcategories = ArrayList(subcategoriesList)
newSubcategories -= subcategoryId
if (newSubcategories.isEmpty()) {
newSubcategoriesMap.remove(category)
}
else {
newSubcategoriesMap[category] = newSubcategories
}
}
}
else {
val newSubcategories = if (subcategoriesList == null) {
ArrayList()
}
else {
ArrayList(subcategoriesList)
}
if (!newSubcategories.contains(subcategoryId)) {
newSubcategories += subcategoryId
}
newSubcategories.sort()
newSubcategoriesMap[category] = newSubcategories
}
return withDisabledSubcategories(newSubcategoriesMap)
}
state.intIncrementModificationCount()
}
class SettingsSyncSettingsState : BaseState() {
var syncEnabled by property(false)
fun isCategoryEnabled(category: SettingsCategory) = !disabledCategories.contains(category)
var disabledCategories by list<SettingsCategory>()
var disabledSubcategories by map<SettingsCategory, ArrayList<String>>()
var migrationFromOldStorageChecked by property(false)
@TestOnly
internal fun reset() {
syncEnabled = false
disabledCategories = mutableListOf()
disabledSubcategories = mutableMapOf()
migrationFromOldStorageChecked = false
fun isSubcategoryEnabled(category: SettingsCategory, subcategoryId: String): Boolean {
val disabled = disabledSubcategories[category]
return disabled == null || !disabled.contains(subcategoryId)
}
}
}
interface SettingsSyncState {
fun isCategoryEnabled(category: SettingsCategory): Boolean
fun setCategoryEnabled(category: SettingsCategory, isEnabled: Boolean)
fun isSubcategoryEnabled(category: SettingsCategory, subcategoryId: String): Boolean
fun setSubcategoryEnabled(category: SettingsCategory, subcategoryId: String, isEnabled: Boolean)
val disabledCategories: List<SettingsCategory>
val disabledSubcategories: Map<SettingsCategory, List<String>>
var syncEnabled: Boolean
var migrationFromOldStorageChecked: Boolean
}
class SettingsSyncStateHolder(initState: SettingsSyncSettings.State = SettingsSyncSettings.State()) : SettingsSyncState {
@Volatile
private var state = initState
override fun isCategoryEnabled(category: SettingsCategory) = state.isCategoryEnabled(category)
override fun setCategoryEnabled(category: SettingsCategory, isEnabled: Boolean) {
state = state.withCategoryEnabled(category, isEnabled)
}
override fun isSubcategoryEnabled(category: SettingsCategory, subcategoryId: String) = state.isSubcategoryEnabled(category, subcategoryId)
override fun setSubcategoryEnabled(category: SettingsCategory, subcategoryId: String, isEnabled: Boolean) {
state = state.withSubcategoryEnabled(category, subcategoryId, isEnabled)
}
override val disabledCategories: List<SettingsCategory>
get() = state.disabledCategories
override val disabledSubcategories: Map<SettingsCategory, List<String>>
get() = state.disabledSubcategories
override var syncEnabled: Boolean
get() = state.syncEnabled
set(value) {
state = state.withSyncEnabled(value)
}
override var migrationFromOldStorageChecked: Boolean
get() = state.migrationFromOldStorageChecked
set(value) {
state = state.withMigrationFromOldStorageChecked(value)
}
}

View File

@@ -3,17 +3,19 @@ package com.intellij.settingsSync.config
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.settingsSync.SettingsSyncBundle.message
import org.jetbrains.annotations.Nls
import com.intellij.settingsSync.SettingsSyncState
import com.intellij.settingsSync.SettingsSyncStateHolder
import java.awt.event.ActionEvent
import javax.swing.AbstractAction
import javax.swing.Action
import javax.swing.JComponent
internal class EnableSettingsSyncDialog
private constructor(parent: JComponent, private val remoteSettingsFound: Boolean) : DialogWrapper(parent, false) {
internal class EnableSettingsSyncDialog(parent: JComponent, remoteSettings: SettingsSyncState?) : DialogWrapper(parent, false) {
private lateinit var configPanel: DialogPanel
private var dialogResult: Result? = null
val syncSettings: SettingsSyncState = remoteSettings ?: SettingsSyncStateHolder()
private val remoteSettingsExist: Boolean = remoteSettings != null
init {
title = message("title.settings.sync")
@@ -25,22 +27,14 @@ internal class EnableSettingsSyncDialog
GET_FROM_SERVER
}
companion object {
fun showAndGetResult(parent: JComponent, remoteSettingsFound: Boolean) : Result? {
val dialog = EnableSettingsSyncDialog(parent, remoteSettingsFound)
dialog.show()
return dialog.getResult()
}
}
override fun createCenterPanel(): JComponent {
configPanel = SettingsSyncPanelFactory.createPanel(message("enable.dialog.select.what.to.sync"))
configPanel = SettingsSyncPanelFactory.createPanel(message("enable.dialog.select.what.to.sync"), syncSettings)
configPanel.reset()
return configPanel
}
override fun createActions(): Array<Action> =
if (remoteSettingsFound) arrayOf(cancelAction, SyncLocalSettingsAction(), GetSettingsFromAccountAction())
if (remoteSettingsExist) arrayOf(cancelAction, SyncLocalSettingsAction(), GetSettingsFromAccountAction())
else {
val enableSyncAction = EnableSyncAction()
enableSyncAction.putValue(DEFAULT_ACTION, true)

View File

@@ -1,76 +0,0 @@
package com.intellij.settingsSync.config
import com.intellij.openapi.components.SettingsCategory
import com.intellij.openapi.components.SettingsCategory.*
import com.intellij.settingsSync.SettingsSyncBundle.message
import com.intellij.settingsSync.SettingsSyncSettings
import org.jetbrains.annotations.Nls
import java.util.*
internal class SettingsCategoryDescriptor(
private val category : SettingsCategory,
val secondaryGroup: SettingsSyncSubcategoryGroup? = null
) {
companion object {
private val DESCRIPTORS : List<SettingsCategoryDescriptor> = listOf(
SettingsCategoryDescriptor(UI, SettingsSyncUiGroup()),
SettingsCategoryDescriptor(KEYMAP),
SettingsCategoryDescriptor(CODE),
SettingsCategoryDescriptor(PLUGINS, SettingsSyncPluginsGroup()),
SettingsCategoryDescriptor(TOOLS),
SettingsCategoryDescriptor(SYSTEM),
)
fun listAll() : List<SettingsCategoryDescriptor> {
return DESCRIPTORS
}
}
var isSynchronized: Boolean = true
fun reset() {
isSynchronized = SettingsSyncSettings.getInstance().isCategoryEnabled(category)
if (secondaryGroup != null) {
secondaryGroup.getDescriptors().forEach {
it.isSelected = isSynchronized && SettingsSyncSettings.getInstance().isSubcategoryEnabled(category, it.id)
}
}
}
fun apply() {
if (secondaryGroup != null) {
secondaryGroup.getDescriptors().forEach {
// !isSynchronized not store disabled states individually
SettingsSyncSettings.getInstance().setSubcategoryEnabled(category, it.id, !isSynchronized || it.isSelected)
}
}
SettingsSyncSettings.getInstance().setCategoryEnabled(category, isSynchronized)
}
fun isModified() : Boolean {
if (isSynchronized != SettingsSyncSettings.getInstance().isCategoryEnabled(category)) return true
if (secondaryGroup != null && isSynchronized) {
secondaryGroup.getDescriptors().forEach {
if (it.isSelected != SettingsSyncSettings.getInstance().isSubcategoryEnabled(category, it.id)) return true
}
}
return false
}
val name: @Nls String
get() {
return message("${categoryKey}.name")
}
val description: @Nls String
get() {
return message("${categoryKey}.description")
}
private val categoryKey: String
get() {
return "settings.category." + category.name.lowercase(Locale.getDefault())
}
}

View File

@@ -73,6 +73,7 @@ internal class SettingsSyncConfigurable : BoundConfigurable(message("title.setti
SettingsSyncEvents.getInstance().addListener(object : SettingsSyncEventListener {
override fun enabledStateChanged(syncEnabled: Boolean) {
listener(invoke())
configPanel.reset()
}
}, disposable!!)
}
@@ -126,7 +127,7 @@ internal class SettingsSyncConfigurable : BoundConfigurable(message("title.setti
}
override fun createPanel(): DialogPanel {
val categoriesPanel = SettingsSyncPanelFactory.createPanel(message("configurable.what.to.sync.label"))
val categoriesPanel = SettingsSyncPanelFactory.createPanel(message("configurable.what.to.sync.label"), SettingsSyncSettings.getInstance())
val authService = SettingsSyncAuthService.getInstance()
val authAvailable = authService.isLoginAvailable()
configPanel = panel {
@@ -252,8 +253,8 @@ internal class SettingsSyncConfigurable : BoundConfigurable(message("title.setti
override fun serverStateCheckFinished(updateResult: UpdateResult) {
when (updateResult) {
NoFileOnServer, FileDeletedFromServer -> showEnableSyncDialog(false)
is Success -> showEnableSyncDialog(true)
NoFileOnServer, FileDeletedFromServer -> showEnableSyncDialog(null)
is Success -> showEnableSyncDialog(updateResult.settingsSnapshot.getState())
is Error -> {
if (updateResult != SettingsSyncEnabler.State.CANCELLED) {
showError(message("notification.title.update.error"), updateResult.message)
@@ -278,19 +279,20 @@ internal class SettingsSyncConfigurable : BoundConfigurable(message("title.setti
updateStatusInfo()
}
private fun showEnableSyncDialog(remoteSettingsFound: Boolean) {
val dialogResult = EnableSettingsSyncDialog.showAndGetResult(configPanel, remoteSettingsFound)
private fun showEnableSyncDialog(remoteSettings: SettingsSyncState?) {
val dialog = EnableSettingsSyncDialog(configPanel, remoteSettings)
dialog.show()
val dialogResult = dialog.getResult()
if (dialogResult != null) {
reset()
when (dialogResult) {
EnableSettingsSyncDialog.Result.GET_FROM_SERVER -> {
syncEnabler.getSettingsFromServer()
syncEnabler.getSettingsFromServer(dialog.syncSettings)
SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.GET_FROM_SERVER)
}
EnableSettingsSyncDialog.Result.PUSH_LOCAL -> {
SettingsSyncSettings.getInstance().syncEnabled = true
syncEnabler.pushSettingsToServer()
if (remoteSettingsFound) {
if (remoteSettings != null) {
SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.PUSH_LOCAL)
}
else {
@@ -302,6 +304,8 @@ internal class SettingsSyncConfigurable : BoundConfigurable(message("title.setti
else {
SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.CANCELED)
}
reset()
configPanel.reset()
}
companion object DisableResult {

View File

@@ -34,7 +34,7 @@ internal class SettingsSyncEnabler {
}
fun getSettingsFromServer() {
fun getSettingsFromServer(syncSettings: SettingsSyncState? = null) {
eventDispatcher.multicaster.updateFromServerStarted()
val settingsSyncControls = SettingsSyncMain.getInstance().controls
object : Task.Modal(null, SettingsSyncBundle.message("enable.sync.get.from.server.progress"), false) {
@@ -44,7 +44,7 @@ internal class SettingsSyncEnabler {
val result = settingsSyncControls.remoteCommunicator.receiveUpdates()
updateResult = result
if (result is UpdateResult.Success) {
val cloudEvent = SyncSettingsEvent.CloudChange(result.settingsSnapshot, result.serverVersionId)
val cloudEvent = SyncSettingsEvent.CloudChange(result.settingsSnapshot, result.serverVersionId, syncSettings)
settingsSyncControls.bridge.initialize(SettingsSyncBridge.InitMode.TakeFromServer(cloudEvent))
}
}
@@ -58,7 +58,7 @@ internal class SettingsSyncEnabler {
fun pushSettingsToServer() {
val settingsSyncControls = SettingsSyncMain.getInstance().controls
object: Task.Modal(null, SettingsSyncBundle.message("enable.sync.push.to.server.progress"), false) {
object : Task.Modal(null, SettingsSyncBundle.message("enable.sync.push.to.server.progress"), false) {
override fun run(indicator: ProgressIndicator) {
// todo initialization must be modal but pushing to server can be made later
settingsSyncControls.bridge.initialize(SettingsSyncBridge.InitMode.PushToServer)

View File

@@ -3,6 +3,7 @@ package com.intellij.settingsSync.config
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.settingsSync.SettingsSyncBundle.message
import com.intellij.settingsSync.SettingsSyncState
import com.intellij.ui.CheckBoxList
import com.intellij.ui.CheckBoxListListener
import com.intellij.ui.SeparatorComponent
@@ -21,52 +22,51 @@ import javax.swing.JComponent
import javax.swing.JPanel
internal object SettingsSyncPanelFactory {
fun createPanel(syncLabel: @Nls String): DialogPanel {
fun createPanel(syncLabel: @Nls String, state: SettingsSyncState): DialogPanel {
return panel {
row {
label(syncLabel)
}
SettingsCategoryDescriptor.listAll().forEach { descriptor ->
descriptor.reset()
val categoryHolders = SyncCategoryHolder.createAllForState(state)
for (holder in categoryHolders) {
indent {
row {
if (descriptor.secondaryGroup == null) {
if (holder.secondaryGroup == null) {
checkBox(
descriptor.name
holder.name
)
.bindSelected(descriptor::isSynchronized)
.onReset { descriptor.reset() }
.onApply { descriptor.apply() }
.onIsModified { descriptor.isModified() }
comment(descriptor.description)
.bindSelected(holder::isSynchronized)
.onReset { holder.reset() }
.onApply { holder.apply() }
.onIsModified { holder.isModified() }
comment(holder.description)
}
else {
val topCheckBox = ThreeStateCheckBox(descriptor.name)
val topCheckBox = ThreeStateCheckBox(holder.name)
topCheckBox.isThirdStateEnabled = false
cell(topCheckBox)
.onReset {
descriptor.reset()
topCheckBox.state = getGroupState(descriptor)
holder.reset()
topCheckBox.state = getGroupState(holder)
}
.onApply {
descriptor.isSynchronized = topCheckBox.state != State.NOT_SELECTED
descriptor.apply()
holder.isSynchronized = topCheckBox.state != State.NOT_SELECTED
holder.apply()
}
.onIsModified { descriptor.isModified() }
val c = comment(descriptor.description).visible(!descriptor.description.isEmpty())
val subcategoryLink = configureLink(descriptor.secondaryGroup, c.component.font.size2D) {
topCheckBox.state = getGroupState(descriptor)
descriptor.isSynchronized = topCheckBox.state != State.NOT_SELECTED
.onIsModified { holder.isModified() }
val c = comment(holder.description).visible(!holder.description.isEmpty())
val subcategoryLink = configureLink(holder.secondaryGroup!!, c.component.font.size2D) {
topCheckBox.state = getGroupState(holder)
holder.isSynchronized = topCheckBox.state != State.NOT_SELECTED
}
cell(subcategoryLink)
.visible(descriptor.secondaryGroup.getDescriptors().size > 1 || !descriptor.secondaryGroup.isComplete())
.visible(holder.secondaryGroup!!.getDescriptors().size > 1 || !holder.secondaryGroup!!.isComplete())
topCheckBox.addActionListener {
descriptor.isSynchronized = topCheckBox.state != State.NOT_SELECTED
descriptor.secondaryGroup.getDescriptors().forEach {
it.isSelected = descriptor.isSynchronized
holder.isSynchronized = topCheckBox.state != State.NOT_SELECTED
holder.secondaryGroup!!.getDescriptors().forEach {
it.isSelected = holder.isSynchronized
}
subcategoryLink.isEnabled = descriptor.secondaryGroup.isComplete() || descriptor.isSynchronized
subcategoryLink.isEnabled = holder.secondaryGroup!!.isComplete() || holder.isSynchronized
}
}
@@ -76,7 +76,7 @@ internal object SettingsSyncPanelFactory {
}
}
private fun getGroupState(descriptor: SettingsCategoryDescriptor): State {
private fun getGroupState(descriptor: SyncCategoryHolder): State {
val group = descriptor.secondaryGroup
if (group == null) {
return if (descriptor.isSynchronized) State.SELECTED else State.NOT_SELECTED
@@ -95,7 +95,7 @@ internal object SettingsSyncPanelFactory {
}
}
private fun configureLink(group: SettingsSyncSubcategoryGroup,
private fun configureLink(group: SyncSubcategoryGroup,
fontSize: Float,
onCheckBoxChange: () -> Unit): JComponent {
val actionLink = ActionLink(message("subcategory.config.link")) {}

View File

@@ -2,7 +2,7 @@ package com.intellij.settingsSync.config
import org.jetbrains.annotations.Nls
internal interface SettingsSyncSubcategoryGroup {
internal interface SyncSubcategoryGroup {
fun getDescriptors() : List<SettingsSyncSubcategoryDescriptor>
/**

View File

@@ -0,0 +1,102 @@
package com.intellij.settingsSync.config
import com.intellij.openapi.components.SettingsCategory
import com.intellij.openapi.components.SettingsCategory.*
import com.intellij.settingsSync.SettingsSyncState
import com.intellij.settingsSync.SettingsSyncBundle.message
import org.jetbrains.annotations.Nls
import java.util.*
internal class SyncCategoryHolder(
val descriptor: Category,
private val state: SettingsSyncState
) {
var isSynchronized: Boolean = state.isCategoryEnabled(descriptor.category)
val name: @Nls String
get() = descriptor.name
val description: @Nls String
get() = descriptor.description
val secondaryGroup: SyncSubcategoryGroup?
get() = descriptor.secondaryGroup
fun reset() {
with(descriptor) {
isSynchronized = state.isCategoryEnabled(category)
if (secondaryGroup != null) {
secondaryGroup.getDescriptors().forEach {
it.isSelected = isSynchronized && state.isSubcategoryEnabled(category, it.id)
}
}
}
}
fun apply() {
with(descriptor) {
if (secondaryGroup != null) {
secondaryGroup.getDescriptors().forEach {
// !isSynchronized not store disabled states individually
state.setSubcategoryEnabled(category, it.id, !isSynchronized || it.isSelected)
}
}
state.setCategoryEnabled(category, isSynchronized)
}
}
fun isModified(): Boolean {
with(descriptor) {
if (isSynchronized != state.isCategoryEnabled(category)) return true
if (secondaryGroup != null && isSynchronized) {
secondaryGroup.getDescriptors().forEach {
if (it.isSelected != state.isSubcategoryEnabled(category, it.id)) return true
}
}
return false
}
}
companion object {
fun createAllForState(state: SettingsSyncState): List<SyncCategoryHolder> {
val retval = arrayListOf<SyncCategoryHolder>()
for (descriptor in Category.DESCRIPTORS) {
retval.add(SyncCategoryHolder(descriptor, state))
}
return retval
}
}
internal class Category(
val category: SettingsCategory,
val secondaryGroup: SyncSubcategoryGroup? = null
) {
val name: @Nls String
get() {
return message("${categoryKey}.name")
}
val description: @Nls String
get() {
return message("${categoryKey}.description")
}
private val categoryKey: String
get() {
return "settings.category." + category.name.lowercase(Locale.getDefault())
}
companion object {
internal val DESCRIPTORS: List<Category> = listOf(
Category(UI, SyncUiGroup()),
Category(KEYMAP),
Category(CODE),
Category(PLUGINS, SyncPluginsGroup()),
Category(TOOLS),
Category(SYSTEM),
)
}
}
}

View File

@@ -8,7 +8,7 @@ import org.jetbrains.annotations.Nls
internal const val BUNDLED_PLUGINS_ID = "bundled"
internal class SettingsSyncPluginsGroup : SettingsSyncSubcategoryGroup {
internal class SyncPluginsGroup : SyncSubcategoryGroup {
private val storedDescriptors = HashMap<String, SettingsSyncSubcategoryDescriptor>()
override fun getDescriptors(): List<SettingsSyncSubcategoryDescriptor> {

View File

@@ -4,7 +4,7 @@ import com.intellij.settingsSync.SettingsSyncBundle
const val EDITOR_FONT_SUBCATEGORY_ID = "editorFont"
internal class SettingsSyncUiGroup : SettingsSyncSubcategoryGroup {
internal class SyncUiGroup : SyncSubcategoryGroup {
private val descriptors = listOf(SettingsSyncSubcategoryDescriptor(SettingsSyncBundle.message("settings.category.ui.editor.font"),
EDITOR_FONT_SUBCATEGORY_ID, false, false))

View File

@@ -145,7 +145,7 @@ internal class SettingsRepositoryToSettingsSyncMigration {
TemplateSettings.getInstance() // Required for live templates to be migrated correctly, see IDEA-303831
SettingsSyncIdeMediatorImpl(ApplicationManager.getApplication().stateStore as ComponentStoreImpl,
PathManager.getConfigDir(), { false }).applyToIde(snapshot)
PathManager.getConfigDir(), { false }).applyToIde(snapshot, null)
settingsRepositoryMigration.showNotificationAboutUnbundling(executorService)
SettingsSyncEventsStatistics.MIGRATED_FROM_SETTINGS_REPOSITORY.log()
}

View File

@@ -13,14 +13,11 @@ import com.intellij.testFramework.junit5.TestApplication
import com.intellij.testFramework.junit5.TestDisposable
import com.intellij.testFramework.replaceService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.test.*
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.BeforeEach
import kotlin.coroutines.CoroutineContext
@OptIn(ExperimentalCoroutinesApi::class)
@TestApplication
@@ -76,7 +73,7 @@ abstract class BasePluginManagerTest {
@BeforeEach
fun setUp() {
SettingsSyncSettings.getInstance().syncEnabled = true
SettingsSyncSettings.getInstance().loadState(SettingsSyncSettings.SettingsSyncSettingsState())
SettingsSyncSettings.getInstance().loadState(SettingsSyncSettings.State())
testPluginManager = TestPluginManager()
ApplicationManager.getApplication().replaceService(PluginManagerProxy::class.java, testPluginManager, testRootDisposable)
testScheduler = TestCoroutineScheduler()

View File

@@ -13,7 +13,7 @@ internal class MockSettingsSyncIdeMediator : SettingsSyncIdeMediator {
private var exceptionToThrowOnApply: Exception? = null
override fun applyToIde(snapshot: SettingsSnapshot) {
override fun applyToIde(snapshot: SettingsSnapshot, settings: SettingsSyncState?) {
if (exceptionToThrowOnApply != null) {
throw exceptionToThrowOnApply!!
}

View File

@@ -0,0 +1,87 @@
package com.intellij.settingsSync
import com.intellij.openapi.components.SettingsCategory
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import java.time.Instant
class SettingsSnapshotTest {
@Test
fun `extract dialog state`() {
val metaInfo = SettingsSnapshot.MetaInfo(Instant.now(), null)
val settingsSyncXmlState = FileState.Modified("options/settingsSync.xml", """
<application>
<component name="SettingsSyncSettings">
<option name="disabledCategories">
<list>
<option value="TOOLS" />
<option value="SYSTEM" />
</list>
</option>
<option name="disabledSubcategories">
<map>
<entry key="UI">
<value>
<list>
<option value="editorFont" />
</list>
</value>
</entry>
<entry key="PLUGINS">
<value>
<list>
<option value="org.vlang" />
</list>
</value>
</entry>
</map>
</option>
<option name="migrationFromOldStorageChecked" value="true" />
<option name="syncEnabled" value="true" />
</component>
</application>
""".trimIndent().toByteArray())
val snapshot = SettingsSnapshot(metaInfo, setOf(settingsSyncXmlState), null, emptyMap(), emptySet())
val state = snapshot.getState()
Assertions.assertTrue(state.disabledCategories.containsAll(listOf(SettingsCategory.SYSTEM, SettingsCategory.TOOLS)))
Assertions.assertEquals(2,state.disabledCategories.size)
Assertions.assertTrue(state.isCategoryEnabled(SettingsCategory.PLUGINS))
Assertions.assertTrue(state.isCategoryEnabled(SettingsCategory.UI))
Assertions.assertFalse(state.isSubcategoryEnabled(SettingsCategory.UI, "editorFont"))
Assertions.assertTrue(state.disabledSubcategories.keys.containsAll(listOf(SettingsCategory.UI, SettingsCategory.PLUGINS)))
Assertions.assertTrue(state.syncEnabled)
Assertions.assertTrue(state.migrationFromOldStorageChecked)
}
@Test
fun `extract dialog state 2`() {
val metaInfo = SettingsSnapshot.MetaInfo(Instant.now(), null)
val settingsSyncXmlState = FileState.Modified("options/settingsSync.xml", """
<application>
<component name="SettingsSyncSettings">
<option name="disabledSubcategories">
<map>
<entry key="PLUGINS">
<value>
<list>
<option value="org.vlang" />
</list>
</value>
</entry>
</map>
</option>
</component>
</application>
""".trimIndent().toByteArray())
val snapshot = SettingsSnapshot(metaInfo, setOf(settingsSyncXmlState), null, emptyMap(), emptySet())
val state = snapshot.getState()
Assertions.assertTrue(state.isCategoryEnabled(SettingsCategory.TOOLS))
Assertions.assertTrue(state.isCategoryEnabled(SettingsCategory.PLUGINS))
Assertions.assertTrue(state.isCategoryEnabled(SettingsCategory.UI))
Assertions.assertTrue(state.isSubcategoryEnabled(SettingsCategory.UI, "editorFont"))
Assertions.assertFalse(state.syncEnabled)
Assertions.assertFalse(state.migrationFromOldStorageChecked)
}
}

View File

@@ -2,21 +2,26 @@ package com.intellij.settingsSync
import com.intellij.configurationStore.ChildlessComponentStore
import com.intellij.configurationStore.StateStorageManager
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.RoamingType
import com.intellij.openapi.components.SettingsCategory
import com.intellij.openapi.components.stateStore
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.intellij.testFramework.rules.InMemoryFsRule
import org.junit.Assert.assertEquals
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import java.nio.file.Path
import java.time.Instant
import kotlin.io.path.createDirectories
import kotlin.io.path.createFile
import kotlin.io.path.div
import kotlin.io.path.pathString
@RunWith(JUnit4::class)
class SettingsSyncIdeMediatorTest {
class SettingsSyncIdeMediatorTest : BasePlatformTestCase() {
@JvmField @Rule
val memoryFs = InMemoryFsRule()
@@ -46,4 +51,50 @@ true
assertEquals(setOf("mytemplate.kt"), visited)
}
@Test
fun `respect SettingSyncState`() {
val rootConfig = memoryFs.fs.getPath("/appconfig")
val componentStore = object : ChildlessComponentStore() {
override val storageManager: StateStorageManager
get() = ApplicationManager.getApplication().stateStore.storageManager
override fun setPath(path: Path) {
TODO("Not yet implemented")
}
}
val mediator = SettingsSyncIdeMediatorImpl(componentStore, rootConfig, { true })
val metaInfo = SettingsSnapshot.MetaInfo(Instant.now(), null)
val settingsSyncXmlState = FileState.Modified("options/settingsSync.xml", """
<application>
<component name="SettingsSyncSettings">
<option name="disabledSubcategories">
<map>
<entry key="PLUGINS">
<value>
<list>
<option value="org.vlang" />
</list>
</value>
</entry>
</map>
</option>
</component>
</application>
""".trimIndent().toByteArray())
val snapshot = SettingsSnapshot(metaInfo, setOf(settingsSyncXmlState), null, emptyMap(), emptySet())
val syncState = SettingsSyncStateHolder(SettingsSyncSettings.State())
syncState.syncEnabled = true
syncState.setCategoryEnabled(SettingsCategory.CODE, false)
syncState.setSubcategoryEnabled(SettingsCategory.PLUGINS, "IdeaVIM", false)
mediator.applyToIde(snapshot, syncState)
Assert.assertTrue(SettingsSyncSettings.getInstance().syncEnabled)
Assert.assertFalse(SettingsSyncSettings.getInstance().migrationFromOldStorageChecked)
Assert.assertFalse(SettingsSyncSettings.getInstance().isCategoryEnabled(SettingsCategory.CODE))
Assert.assertTrue(SettingsSyncSettings.getInstance().isCategoryEnabled(SettingsCategory.UI))
Assert.assertTrue(SettingsSyncSettings.getInstance().isCategoryEnabled(SettingsCategory.SYSTEM))
Assert.assertTrue(SettingsSyncSettings.getInstance().isSubcategoryEnabled(SettingsCategory.PLUGINS, "org.vlang"))
Assert.assertFalse(SettingsSyncSettings.getInstance().isSubcategoryEnabled(SettingsCategory.PLUGINS, "IdeaVIM"))
}
}

View File

@@ -52,7 +52,7 @@ internal abstract class SettingsSyncTestBase {
configDir = mainDir.resolve("rootconfig").createDirectories()
SettingsSyncLocalSettings.getInstance().state.reset()
SettingsSyncSettings.getInstance().state.reset()
SettingsSyncSettings.getInstance().state = SettingsSyncSettings.State()
remoteCommunicator = if (isTestingAgainstRealCloudServer()) {
TestRemoteCommunicator()