diff --git a/src/WindowController.App/SyncManager.cs b/src/WindowController.App/SyncManager.cs index b67288b..726407a 100644 --- a/src/WindowController.App/SyncManager.cs +++ b/src/WindowController.App/SyncManager.cs @@ -118,6 +118,7 @@ public void UpdateHooksIfNeeded(bool skipRebuild = false) { _hookManager.Uninstall(); _syncGroups.Clear(); + _profileNames.Clear(); _lastMinMaxByHwnd.Clear(); } } diff --git a/src/WindowController.App/ViewModels/MainViewModel.cs b/src/WindowController.App/ViewModels/MainViewModel.cs index 1cc12b5..cea74d8 100644 --- a/src/WindowController.App/ViewModels/MainViewModel.cs +++ b/src/WindowController.App/ViewModels/MainViewModel.cs @@ -45,6 +45,7 @@ public partial class MainViewModel : ObservableObject private readonly SyncManager _syncManager; private readonly AppSettingsStore _appSettings; private readonly ILogger _log; + private bool _isUpdatingProfileName; [ObservableProperty] private string _statusText = ""; [ObservableProperty] private string _profileName = ""; @@ -447,13 +448,36 @@ public void ReloadProfiles() private void OnProfileNameChanged(ProfileItem pi) { + // Prevent re-entrancy when we programmatically update pi.Name + if (_isUpdatingProfileName) + return; + + // Get the current profile from the store + var current = _store.FindById(pi.Id); + if (current == null) + { + StatusText = "プロファイルが見つかりません。"; + return; + } + + // If the name has not actually changed compared to the store, ignore. + if (string.Equals(current.Name, pi.Name, StringComparison.Ordinal)) + return; + + var newName = pi.Name?.Trim(); if (string.IsNullOrEmpty(newName)) { // Revert to current stored name - var current = _store.FindById(pi.Id); - if (current != null) + _isUpdatingProfileName = true; + try + { pi.Name = current.Name; + } + finally + { + _isUpdatingProfileName = false; + } StatusText = "プロファイル名を空にすることはできません。"; return; } @@ -467,7 +491,17 @@ private void OnProfileNameChanged(ProfileItem pi) // If the name was adjusted due to conflict, update the UI if (finalName != newName) - pi.Name = finalName; + { + _isUpdatingProfileName = true; + try + { + pi.Name = finalName; + } + finally + { + _isUpdatingProfileName = false; + } + } _syncManager.ScheduleRebuild(); StatusText = $"名前を変更しました: {finalName}"; diff --git a/src/WindowController.Core.Tests/ProfileStoreTests.cs b/src/WindowController.Core.Tests/ProfileStoreTests.cs index d20aada..0076f21 100644 --- a/src/WindowController.Core.Tests/ProfileStoreTests.cs +++ b/src/WindowController.Core.Tests/ProfileStoreTests.cs @@ -17,7 +17,14 @@ public ProfileStoreTests() public void Dispose() { - try { Directory.Delete(_tempDir, true); } catch { } + try + { + Directory.Delete(_tempDir, true); + } + catch (Exception ex) + { + _log.Warning(ex, "Failed to delete temporary test directory {TempDir}", _tempDir); + } } private string TempFile() => Path.Combine(_tempDir, "profiles.json"); diff --git a/src/WindowController.Core/ProfileStore.cs b/src/WindowController.Core/ProfileStore.cs index 98412e5..05224e9 100644 --- a/src/WindowController.Core/ProfileStore.cs +++ b/src/WindowController.Core/ProfileStore.cs @@ -62,15 +62,16 @@ public void Load() { var json = File.ReadAllText(_filePath); var parsed = JsonSerializer.Deserialize(json, JsonOptions); + + // Detect migration need before normalization + bool needsMigration = parsed != null && parsed.Profiles.Any(p => + string.IsNullOrEmpty(p.Id) || + parsed.Profiles.Count(x => x.Id == p.Id) > 1); + var normalized = NormalizeData(parsed ?? new ProfilesRoot()); - - // Check if any profile received a new Id during normalization (migration) - bool needsSave = json != null && normalized.Profiles.Any(p => - !json.Contains($"\"id\": \"{p.Id}\"", StringComparison.Ordinal)); - Data = normalized; - if (needsSave) + if (needsMigration) Save(); } catch (Exception ex) @@ -153,7 +154,13 @@ public bool DeleteProfileById(string id) var profile = FindById(profileId); if (profile == null) return null; - var finalName = ResolveUniqueName(desiredName, profileId); + var trimmed = desiredName.Trim(); + var finalName = ResolveUniqueName(trimmed, profileId); + + // Skip save if the name hasn't actually changed + if (finalName == profile.Name) + return finalName; + profile.Name = finalName; profile.UpdatedAt = DateTime.Now.ToString("yyyy-MM-dd'T'HH:mm:ss"); Save();