diff --git a/Source/NETworkManager.Controls/GroupExpandedChangedArgs.cs b/Source/NETworkManager.Controls/GroupExpandedChangedArgs.cs new file mode 100644 index 0000000000..975ec5f6cf --- /dev/null +++ b/Source/NETworkManager.Controls/GroupExpandedChangedArgs.cs @@ -0,0 +1,10 @@ +using System; + +namespace NETworkManager.Controls +{ + public class GroupExpandedChangedArgs(string groupName, bool isExpanded) : EventArgs + { + public string GroupName { get; } = groupName; + public bool IsExpanded { get; } = isExpanded; + } +} diff --git a/Source/NETworkManager.Controls/GroupExpander.cs b/Source/NETworkManager.Controls/GroupExpander.cs index 3bfd0c1f44..d5105f0908 100644 --- a/Source/NETworkManager.Controls/GroupExpander.cs +++ b/Source/NETworkManager.Controls/GroupExpander.cs @@ -8,7 +8,7 @@ public class GroupExpander : Expander { public static readonly DependencyProperty StateStoreProperty = DependencyProperty.Register( - "StateStore", + nameof(StateStore), typeof(GroupExpanderStateStore), typeof(GroupExpander), new PropertyMetadata(null, OnStateStoreChanged)); diff --git a/Source/NETworkManager.Controls/GroupExpanderStateStore.cs b/Source/NETworkManager.Controls/GroupExpanderStateStore.cs index 76eea8e940..2aead547fe 100644 --- a/Source/NETworkManager.Controls/GroupExpanderStateStore.cs +++ b/Source/NETworkManager.Controls/GroupExpanderStateStore.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; @@ -8,6 +9,11 @@ public class GroupExpanderStateStore : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; + /// + /// Occurs when a single group's expansion state changes via the indexer (not raised by ). + /// + public event EventHandler GroupExpandedChanged; + protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); @@ -40,7 +46,25 @@ public bool this[string groupName] _states[groupName] = value; OnPropertyChanged($"Item[{groupName}]"); + GroupExpandedChanged?.Invoke(this, new GroupExpandedChangedArgs(groupName, value)); } } + + /// + /// Replaces all stored states with the given ones (e.g. bulk-loading previously persisted state). + /// Does not raise or - consumers + /// (e.g. ) read the current state fresh whenever they bind, so no + /// notification is needed, and this avoids a load-triggers-save feedback loop. + /// + public void LoadState(IReadOnlyDictionary state) + { + _states.Clear(); + + if (state == null) + return; + + foreach (var kvp in state) + _states[kvp.Key] = kvp.Value; + } } } diff --git a/Source/NETworkManager.Profiles/ProfileInfo.cs b/Source/NETworkManager.Profiles/ProfileInfo.cs index 80cb4937a4..209b19d33b 100644 --- a/Source/NETworkManager.Profiles/ProfileInfo.cs +++ b/Source/NETworkManager.Profiles/ProfileInfo.cs @@ -222,10 +222,6 @@ public ProfileInfo(ProfileInfo profile) SNMP_PrivacyProvider = profile.SNMP_PrivacyProvider; SNMP_Priv = profile.SNMP_Priv; - // Firewall - Firewall_Enabled = profile.Firewall_Enabled; - //Firewall_Rules = profile.Firewall_Rules; - // Wake on LAN WakeOnLAN_Enabled = profile.WakeOnLAN_Enabled; WakeOnLAN_MACAddress = profile.WakeOnLAN_MACAddress; @@ -486,9 +482,6 @@ public ProfileInfo(ProfileInfo profile) public SNMPV3PrivacyProvider SNMP_PrivacyProvider { get; set; } = GlobalStaticConfiguration.SNMP_PrivacyProvider; [XmlIgnore] public SecureString SNMP_Priv { get; set; } - public bool Firewall_Enabled { get; set; } - //public List Firewall_Rules { get; set; } - public bool WakeOnLAN_Enabled { get; set; } public string WakeOnLAN_MACAddress { get; set; } public string WakeOnLAN_Broadcast { get; set; } diff --git a/Source/NETworkManager.Profiles/ProfileManager.cs b/Source/NETworkManager.Profiles/ProfileManager.cs index 5dc48df2be..38ed36f408 100644 --- a/Source/NETworkManager.Profiles/ProfileManager.cs +++ b/Source/NETworkManager.Profiles/ProfileManager.cs @@ -381,6 +381,8 @@ public static void RenameProfileFile(ProfileFileInfo profileFileInfo, string new File.Copy(profileFileInfo.Path, newProfileFileInfo.Path); ProfileFiles.Add(newProfileFileInfo); + MigrateGroupExpandState(profileFileInfo.Path, newProfileFileInfo.Path); + // Switch profile, if it was previously loaded if (switchProfile) { @@ -407,6 +409,8 @@ public static void DeleteProfileFile(ProfileFileInfo profileFileInfo) if (LoadedProfileFile != null && LoadedProfileFile.Equals(profileFileInfo)) LoadedProfileFileChanged(ProfileFiles.FirstOrDefault(x => !x.Equals(profileFileInfo))); + RemoveGroupExpandState(profileFileInfo.Path); + File.Delete(profileFileInfo.Path); ProfileFiles.Remove(profileFileInfo); } @@ -478,6 +482,8 @@ public static void EnableEncryption(ProfileFileInfo profileFileInfo, SecureStrin // Add the new profile ProfileFiles.Add(newProfileFileInfo); + MigrateGroupExpandState(profileFileInfo.Path, newProfileFileInfo.Path); + // Switch profile, if it was previously loaded if (switchProfile) { @@ -563,6 +569,8 @@ public static void ChangeMasterPassword(ProfileFileInfo profileFileInfo, SecureS // Add the new profile ProfileFiles.Add(newProfileFileInfo); + MigrateGroupExpandState(profileFileInfo.Path, newProfileFileInfo.Path); + // Switch profile, if it was previously loaded if (switchProfile) { @@ -632,6 +640,8 @@ public static void DisableEncryption(ProfileFileInfo profileFileInfo, SecureStri // Add the new profile ProfileFiles.Add(newProfileFileInfo); + MigrateGroupExpandState(profileFileInfo.Path, newProfileFileInfo.Path); + // Switch profile, if it was previously loaded if (switchProfile) { @@ -739,6 +749,8 @@ private static void Load(ProfileFileInfo profileFileInfo) // Add the new profile ProfileFiles.Add(newProfileFileInfo); + MigrateGroupExpandState(profileFileInfo.Path, newProfileFileInfo.Path); + // Switch profile Log.Info($"Switching to migrated profile file: {newProfileFileInfo.Path}."); Switch(newProfileFileInfo, false); @@ -849,6 +861,70 @@ public static void Switch(ProfileFileInfo info, bool saveLoadedProfiles = true) #endregion + #region Group expand state persistence + + /// + /// Moves the persisted group-expand state (see ) + /// from the old to the new profile file path. No-op if the paths are equal or there is no state for + /// the old path. + /// + private static void MigrateGroupExpandState(string oldPath, string newPath) + { + if (oldPath == newPath) + return; + + if (SettingsManager.Current.Profiles_GroupExpandState.Remove(oldPath, out var state)) + { + SettingsManager.Current.Profiles_GroupExpandState[newPath] = state; + SettingsManager.Current.SettingsChanged = true; + } + } + + /// + /// Removes the persisted group-expand state for a profile file that no longer exists. + /// + private static void RemoveGroupExpandState(string path) + { + if (SettingsManager.Current.Profiles_GroupExpandState.Remove(path)) + SettingsManager.Current.SettingsChanged = true; + } + + /// + /// Renames the group key inside the persisted group-expand state for the currently loaded profile file. + /// + private static void MigrateGroupExpandStateKey(string oldGroupName, string newGroupName) + { + if (oldGroupName == newGroupName) + return; + + if (LoadedProfileFile == null) + return; + + if (!SettingsManager.Current.Profiles_GroupExpandState.TryGetValue(LoadedProfileFile.Path, out var state)) + return; + + if (!state.Remove(oldGroupName, out var isExpanded)) + return; + + state[newGroupName] = isExpanded; + SettingsManager.Current.SettingsChanged = true; + } + + /// + /// Removes the group key from the persisted group-expand state for the currently loaded profile file. + /// + private static void RemoveGroupExpandStateKey(string groupName) + { + if (LoadedProfileFile == null) + return; + + if (SettingsManager.Current.Profiles_GroupExpandState.TryGetValue(LoadedProfileFile.Path, out var state) + && state.Remove(groupName)) + SettingsManager.Current.SettingsChanged = true; + } + + #endregion + #region Serialize and deserialize /// @@ -1085,6 +1161,8 @@ public static void ReplaceGroup(GroupInfo oldGroup, GroupInfo newGroup) LoadedProfileFileData.Groups.Remove(oldGroup); LoadedProfileFileData.Groups.Add(newGroup); + MigrateGroupExpandStateKey(oldGroup.Name, newGroup.Name); + ProfilesUpdated(); } @@ -1099,6 +1177,8 @@ public static void RemoveGroup(GroupInfo group) LoadedProfileFileData.Groups.Remove(group); + RemoveGroupExpandStateKey(group.Name); + ProfilesUpdated(); } diff --git a/Source/NETworkManager.Settings/SettingsInfo.cs b/Source/NETworkManager.Settings/SettingsInfo.cs index 81931a83df..eb33ec907c 100644 --- a/Source/NETworkManager.Settings/SettingsInfo.cs +++ b/Source/NETworkManager.Settings/SettingsInfo.cs @@ -10,6 +10,7 @@ using NETworkManager.Utilities; using NETworkManager.Utilities.ActiveDirectory; using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; @@ -566,6 +567,25 @@ public string Profiles_LastSelected } } + /// + /// Persisted collapsed/expanded state of profile groups, shared across all tools. Outer key is the + /// full profile file path, inner key is the group name. Mutated in place by callers - since nested + /// dictionary mutations don't raise , callers must explicitly set + /// themselves. + /// + public Dictionary> Profiles_GroupExpandState + { + get; + set + { + if (value == field) + return; + + field = value; + OnPropertyChanged(); + } + } = []; + public bool Profiles_IsDailyBackupEnabled { get; @@ -3198,32 +3218,6 @@ public ExportFileType HostsFileEditor_ExportFileType #endregion #region Firewall - public bool Firewall_ExpandProfileView - { - get; - set - { - if (value == field) - return; - - field = value; - OnPropertyChanged(); - } - } = GlobalStaticConfiguration.Profile_ExpandProfileView; - - public double Firewall_ProfileWidth - { - get; - set - { - if (Math.Abs(value - field) < GlobalStaticConfiguration.FloatPointFix) - return; - - field = value; - OnPropertyChanged(); - } - } = GlobalStaticConfiguration.Profile_DefaultWidthExpanded; - public string Firewall_ExportFilePath { get; diff --git a/Source/NETworkManager/ProfileDialogManager.cs b/Source/NETworkManager/ProfileDialogManager.cs index 4eb52230c1..f2e733c241 100644 --- a/Source/NETworkManager/ProfileDialogManager.cs +++ b/Source/NETworkManager/ProfileDialogManager.cs @@ -256,9 +256,6 @@ private static ProfileInfo ParseProfileInfo(ProfileViewModel instance) ? instance.SNMP_Priv : new SecureString(), - // Firewall - Firewall_Enabled = instance.Firewall_Enabled, - // Wake on LAN WakeOnLAN_Enabled = instance.WakeOnLAN_Enabled, WakeOnLAN_MACAddress = instance.WakeOnLAN_MACAddress?.Trim(), diff --git a/Source/NETworkManager/ViewModels/FirewallViewModel.cs b/Source/NETworkManager/ViewModels/FirewallViewModel.cs index 18d8471edd..384759b2a0 100644 --- a/Source/NETworkManager/ViewModels/FirewallViewModel.cs +++ b/Source/NETworkManager/ViewModels/FirewallViewModel.cs @@ -1,4 +1,4 @@ -using log4net; +using log4net; using MahApps.Metro.Controls; using MahApps.Metro.SimpleChildWindow; using NETworkManager.Localization.Resources; @@ -17,27 +17,19 @@ using System.Windows; using System.Windows.Data; using System.Windows.Input; -using System.Windows.Threading; namespace NETworkManager.ViewModels; -using Controls; -using Profiles; -using Models; - /// /// ViewModel for the Firewall application. /// -public class FirewallViewModel : ViewModelBase, IProfileManager +public class FirewallViewModel : ViewModelBase { #region Variables private static readonly ILog Log = LogManager.GetLogger(typeof(FirewallViewModel)); - private readonly DispatcherTimer _searchDispatcherTimer = new(); - private bool _searchDisabled; private readonly bool _isLoading; - private bool _isViewActive = true; #region Rules @@ -150,208 +142,6 @@ private set #endregion - #region Profiles - - /// - /// Gets the collection view of profiles. - /// - public ICollectionView Profiles - { - get; - private set - { - if (value == field) - return; - - field = value; - OnPropertyChanged(); - } - } - - /// - /// Gets or sets the selected profile. - /// - public ProfileInfo SelectedProfile - { - get; - set - { - if (value == field) - return; - - field = value; - OnPropertyChanged(); - } - } = new(); - - /// - /// Gets or sets the search text. - /// - public string Search - { - get; - set - { - if (value == field) - return; - - field = value; - - // Start searching... - if (!_searchDisabled) - { - IsSearching = true; - _searchDispatcherTimer.Start(); - } - - OnPropertyChanged(); - } - } - - /// - /// Gets or sets a value indicating whether a search is in progress. - /// - public bool IsSearching - { - get; - set - { - if (value == field) - return; - - field = value; - OnPropertyChanged(); - } - } - - /// - /// Gets or sets a value indicating whether the profile filter is open. - /// - public bool ProfileFilterIsOpen - { - get; - set - { - if (value == field) - return; - - field = value; - OnPropertyChanged(); - } - } - - /// - /// Gets the collection view for profile filter tags. - /// - public ICollectionView ProfileFilterTagsView { get; } - - /// - /// Gets the collection of profile filter tags. - /// - private ObservableCollection ProfileFilterTags { get; } = []; - - /// - /// Gets or sets a value indicating whether any tag match is sufficient for filtering. - /// - public bool ProfileFilterTagsMatchAny - { - get; - set - { - if (value == field) - return; - - field = value; - OnPropertyChanged(); - } - } = GlobalStaticConfiguration.Profile_TagsMatchAny; - - /// - /// Gets or sets a value indicating whether all tags must match for filtering. - /// - public bool ProfileFilterTagsMatchAll - { - get; - set - { - if (value == field) - return; - - field = value; - OnPropertyChanged(); - } - } - - /// - /// Gets or sets a value indicating whether a profile filter is set. - /// - public bool IsProfileFilterSet - { - get; - set - { - if (value == field) - return; - - field = value; - OnPropertyChanged(); - } - } - - public GroupExpanderStateStore GroupExpanderStateStore { get; } = new(); - - private bool _canProfileWidthChange = true; - private double _tempProfileWidth; - - /// - /// Gets or sets a value indicating whether the profile view is expanded. - /// - public bool ExpandProfileView - { - get; - set - { - if (value == field) - return; - - if (!_isLoading) - SettingsManager.Current.Firewall_ExpandProfileView = value; - - field = value; - - if (_canProfileWidthChange) - ResizeProfile(false); - - OnPropertyChanged(); - } - } - - /// - /// Gets or sets the width of the profile view. - /// - public GridLength ProfileWidth - { - get; - set - { - if (value == field) - return; - - if (!_isLoading && Math.Abs(value.Value - GlobalStaticConfiguration.Profile_WidthCollapsed) > - GlobalStaticConfiguration.FloatPointFix) // Do not save the size when collapsed - SettingsManager.Current.Firewall_ProfileWidth = value.Value; - - field = value; - - if (_canProfileWidthChange) - ResizeProfile(true); - - OnPropertyChanged(); - } - } - - #endregion - #endregion #region Constructor, load settings @@ -382,39 +172,9 @@ public FirewallViewModel() // Load firewall rules _ = Refresh(true); - // Profiles - CreateTags(); - - ProfileFilterTagsView = CollectionViewSource.GetDefaultView(ProfileFilterTags); - ProfileFilterTagsView.SortDescriptions.Add(new SortDescription(nameof(ProfileFilterTagsInfo.Name), - ListSortDirection.Ascending)); - - SetProfilesView(new ProfileFilterInfo()); - - ProfileManager.OnProfilesUpdated += ProfileManager_OnProfilesUpdated; - - _searchDispatcherTimer.Interval = GlobalStaticConfiguration.SearchDispatcherTimerTimeSpan; - _searchDispatcherTimer.Tick += SearchDispatcherTimer_Tick; - - LoadSettings(); - _isLoading = false; } - /// - /// Loads the settings. - /// - private void LoadSettings() - { - ExpandProfileView = SettingsManager.Current.Firewall_ExpandProfileView; - - ProfileWidth = ExpandProfileView - ? new GridLength(SettingsManager.Current.Firewall_ProfileWidth) - : new GridLength(GlobalStaticConfiguration.Profile_WidthCollapsed); - - _tempProfileWidth = SettingsManager.Current.Firewall_ProfileWidth; - } - #endregion #region ICommand & Actions @@ -689,159 +449,6 @@ await DialogHelper.ShowMessageAsync(Application.Current.MainWindow, Strings.Erro return Application.Current.MainWindow.ShowChildWindowAsync(childWindow); } - /// - /// Gets the command to add a new profile. - /// - public ICommand AddProfileCommand => new RelayCommand(_ => AddProfileAction()); - - /// - /// Action to add a new profile. - /// - private void AddProfileAction() - { - _ = ProfileDialogManager - .ShowAddProfileDialog(Application.Current.MainWindow, this, null, null, ApplicationName.Firewall); - } - - /// - /// Checks if the profile modification commands can be executed. - /// - private bool ModifyProfile_CanExecute(object obj) - { - return SelectedProfile is { IsDynamic: false }; - } - - /// - /// Gets the command to edit the selected profile. - /// - public ICommand EditProfileCommand => new RelayCommand(_ => EditProfileAction(), ModifyProfile_CanExecute); - - /// - /// Action to edit the selected profile. - /// - private void EditProfileAction() - { - _ = ProfileDialogManager.ShowEditProfileDialog(Application.Current.MainWindow, this, SelectedProfile); - } - - /// - /// Action to copy the selected profile as a new profile. - /// - public ICommand CopyAsProfileCommand => new RelayCommand(_ => CopyAsProfileAction(), ModifyProfile_CanExecute); - - /// - /// Action to copy the selected profile as a new profile. - /// - private void CopyAsProfileAction() - { - _ = ProfileDialogManager.ShowCopyAsProfileDialog(Application.Current.MainWindow, this, SelectedProfile); - } - - /// - /// Gets the command to delete the selected profile. - /// - public ICommand DeleteProfileCommand => new RelayCommand(_ => DeleteProfileAction(), ModifyProfile_CanExecute); - - /// - /// Action to delete the selected profile. - /// - private void DeleteProfileAction() - { - _ = ProfileDialogManager - .ShowDeleteProfileDialog(Application.Current.MainWindow, this, new List { SelectedProfile }); - } - - /// - /// Gets the command to edit a profile group. - /// - public ICommand EditGroupCommand => new RelayCommand(EditGroupAction); - - /// - /// Action to edit a profile group. - /// - private void EditGroupAction(object group) - { - _ = ProfileDialogManager - .ShowEditGroupDialog(Application.Current.MainWindow, this, ProfileManager.GetGroupByName($"{group}")); - } - - /// - /// Gets the command to open the profile filter. - /// - public ICommand OpenProfileFilterCommand => new RelayCommand(_ => OpenProfileFilterAction()); - - /// - /// Action to open the profile filter. - /// - private void OpenProfileFilterAction() - { - ProfileFilterIsOpen = true; - } - - /// - /// Gets the command to apply the profile filter. - /// - public ICommand ApplyProfileFilterCommand => new RelayCommand(_ => ApplyProfileFilterAction()); - - /// - /// Action to apply the profile filter. - /// - private void ApplyProfileFilterAction() - { - RefreshProfiles(); - - ProfileFilterIsOpen = false; - } - - /// - /// Gets the command to clear the profile filter. - /// - public ICommand ClearProfileFilterCommand => new RelayCommand(_ => ClearProfileFilterAction()); - - /// - /// Action to clear the profile filter. - /// - private void ClearProfileFilterAction() - { - _searchDisabled = true; - Search = string.Empty; - _searchDisabled = false; - - foreach (var tag in ProfileFilterTags) - tag.IsSelected = false; - - RefreshProfiles(); - - IsProfileFilterSet = false; - ProfileFilterIsOpen = false; - } - - /// - /// Gets the command to expand all profile groups. - /// - public ICommand ExpandAllProfileGroupsCommand => new RelayCommand(_ => ExpandAllProfileGroupsAction()); - - /// - /// Action to expand all profile groups. - /// - private void ExpandAllProfileGroupsAction() - { - SetIsExpandedForAllProfileGroups(true); - } - - /// - /// Gets the command to collapse all profile groups. - /// - public ICommand CollapseAllProfileGroupsCommand => new RelayCommand(_ => CollapseAllProfileGroupsAction()); - - /// - /// Action to collapse all profile groups. - /// - private void CollapseAllProfileGroupsAction() - { - SetIsExpandedForAllProfileGroups(false); - } - #region Additional commands /// @@ -984,57 +591,11 @@ private static List ParseAddressesString(string value) return [.. value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)]; } - /// - /// Sets the IsExpanded property for all profile groups. - /// - /// The value to set. - private void SetIsExpandedForAllProfileGroups(bool isExpanded) - { - foreach (var group in Profiles.Groups.Cast()) - GroupExpanderStateStore[group.Name.ToString()] = isExpanded; - } - - /// - /// Resizes the profile view. - /// - /// Indicates whether the resize is due to a size change. - private void ResizeProfile(bool dueToChangedSize) - { - _canProfileWidthChange = false; - - if (dueToChangedSize) - { - ExpandProfileView = Math.Abs(ProfileWidth.Value - GlobalStaticConfiguration.Profile_WidthCollapsed) > - GlobalStaticConfiguration.FloatPointFix; - } - else - { - if (ExpandProfileView) - { - ProfileWidth = - Math.Abs(_tempProfileWidth - GlobalStaticConfiguration.Profile_WidthCollapsed) < - GlobalStaticConfiguration.FloatPointFix - ? new GridLength(GlobalStaticConfiguration.Profile_DefaultWidthExpanded) - : new GridLength(_tempProfileWidth); - } - else - { - _tempProfileWidth = ProfileWidth.Value; - ProfileWidth = new GridLength(GlobalStaticConfiguration.Profile_WidthCollapsed); - } - } - - _canProfileWidthChange = true; - } - /// /// Called when the view becomes visible. /// public void OnViewVisible() { - _isViewActive = true; - - RefreshProfiles(); } /// @@ -1042,111 +603,6 @@ public void OnViewVisible() /// public void OnViewHide() { - _isViewActive = false; - } - - /// - /// Creates the profile filter tags. - /// - private void CreateTags() - { - var tags = ProfileManager.LoadedProfileFileData.Groups.SelectMany(x => x.Profiles).Where(x => x.Firewall_Enabled) - .SelectMany(x => x.TagsCollection).Distinct().ToList(); - - var tagSet = new HashSet(tags); - - for (var i = ProfileFilterTags.Count - 1; i >= 0; i--) - { - if (!tagSet.Contains(ProfileFilterTags[i].Name)) - ProfileFilterTags.RemoveAt(i); - } - - var existingTagNames = new HashSet(ProfileFilterTags.Select(ft => ft.Name)); - - foreach (var tag in tags.Where(tag => !existingTagNames.Contains(tag))) - { - ProfileFilterTags.Add(new ProfileFilterTagsInfo(false, tag)); - } - } - - /// - /// Sets the profiles view with the specified filter. - /// - /// The profile filter. - /// The profile to select. - private void SetProfilesView(ProfileFilterInfo filter, ProfileInfo profile = null) - { - Profiles = new CollectionViewSource - { - Source = ProfileManager.LoadedProfileFileData.Groups.SelectMany(x => x.Profiles).Where(x => x.Firewall_Enabled && ( - string.IsNullOrEmpty(filter.Search) || - x.Name.IndexOf(filter.Search, StringComparison.OrdinalIgnoreCase) > -1) && ( - // If no tags are selected, show all profiles - (!filter.Tags.Any()) || - // Any tag can match - (filter.TagsFilterMatch == ProfileFilterTagsMatch.Any && - filter.Tags.Any(tag => x.TagsCollection.Contains(tag))) || - // All tags must match - (filter.TagsFilterMatch == ProfileFilterTagsMatch.All && - filter.Tags.All(tag => x.TagsCollection.Contains(tag)))) - ).OrderBy(x => x.Group).ThenBy(x => x.Name) - }.View; - - Profiles.GroupDescriptions.Add(new PropertyGroupDescription(nameof(ProfileInfo.Group))); - - // Set specific profile or first if null - SelectedProfile = null; - - if (profile != null) - SelectedProfile = Profiles.Cast().FirstOrDefault(x => x.Equals(profile)) ?? - Profiles.Cast().FirstOrDefault(); - else - SelectedProfile = Profiles.Cast().FirstOrDefault(); - } - - /// - /// Refreshes the profiles. - /// - private void RefreshProfiles() - { - if (!_isViewActive) - return; - - var filter = new ProfileFilterInfo - { - Search = Search, - Tags = [.. ProfileFilterTags.Where(x => x.IsSelected).Select(x => x.Name)], - TagsFilterMatch = ProfileFilterTagsMatchAny ? ProfileFilterTagsMatch.Any : ProfileFilterTagsMatch.All - }; - - SetProfilesView(filter, SelectedProfile); - - IsProfileFilterSet = !string.IsNullOrEmpty(filter.Search) || filter.Tags.Any(); - } - #endregion - - #region Events - - /// - /// Handles the OnProfilesUpdated event of the ProfileManager. - /// - private void ProfileManager_OnProfilesUpdated(object sender, EventArgs e) - { - CreateTags(); - - RefreshProfiles(); - } - - /// - /// Handles the Tick event of the search dispatcher timer. - /// - private void SearchDispatcherTimer_Tick(object sender, EventArgs e) - { - _searchDispatcherTimer.Stop(); - - RefreshProfiles(); - - IsSearching = false; } #endregion diff --git a/Source/NETworkManager/ViewModels/ProfileHostViewModelBase.cs b/Source/NETworkManager/ViewModels/ProfileHostViewModelBase.cs index 1a55ab14df..b8c1d66bf1 100644 --- a/Source/NETworkManager/ViewModels/ProfileHostViewModelBase.cs +++ b/Source/NETworkManager/ViewModels/ProfileHostViewModelBase.cs @@ -204,6 +204,8 @@ public bool IsProfileFilterSet /// protected void InitializeProfileHost() { + LoadGroupExpandState(); + CreateTags(); ProfileFilterTagsView = CollectionViewSource.GetDefaultView(ProfileFilterTags); @@ -213,6 +215,7 @@ protected void InitializeProfileHost() SetProfilesView(new ProfileFilterInfo()); ProfileManager.OnProfilesUpdated += ProfileManager_OnProfilesUpdated; + GroupExpanderStateStore.GroupExpandedChanged += GroupExpanderStateStore_GroupExpandedChanged; _searchDispatcherTimer.Interval = GlobalStaticConfiguration.SearchDispatcherTimerTimeSpan; _searchDispatcherTimer.Tick += SearchDispatcherTimer_Tick; @@ -352,6 +355,22 @@ private void SetIsExpandedForAllProfileGroups(bool isExpanded) GroupExpanderStateStore[group.Name.ToString()] = isExpanded; } + /// + /// Loads the persisted group-expand state for the currently loaded profile file into + /// . Always called, even with no persisted state, so that + /// switching to a different profile file doesn't leak state for a same-named group. + /// + private void LoadGroupExpandState() + { + Dictionary state = null; + + if (ProfileManager.LoadedProfileFile != null) + SettingsManager.Current.Profiles_GroupExpandState.TryGetValue( + ProfileManager.LoadedProfileFile.Path, out state); + + GroupExpanderStateStore.LoadState(state); + } + /// /// Called when the view becomes visible. /// @@ -359,6 +378,8 @@ public virtual void OnViewVisible() { _isViewActive = true; + LoadGroupExpandState(); + RefreshProfiles(); } @@ -466,6 +487,23 @@ private void ProfileManager_OnProfilesUpdated(object sender, EventArgs e) RefreshProfiles(); } + private void GroupExpanderStateStore_GroupExpandedChanged(object sender, GroupExpandedChangedArgs e) + { + if (ProfileManager.LoadedProfileFile == null) + return; + + var path = ProfileManager.LoadedProfileFile.Path; + + if (!SettingsManager.Current.Profiles_GroupExpandState.TryGetValue(path, out var state)) + { + state = new Dictionary(); + SettingsManager.Current.Profiles_GroupExpandState[path] = state; + } + + state[e.GroupName] = e.IsExpanded; + SettingsManager.Current.SettingsChanged = true; + } + private void SearchDispatcherTimer_Tick(object sender, EventArgs e) { _searchDispatcherTimer.Stop(); diff --git a/Source/NETworkManager/ViewModels/ProfileViewModel.cs b/Source/NETworkManager/ViewModels/ProfileViewModel.cs index 29b84def39..4c6ea972fb 100644 --- a/Source/NETworkManager/ViewModels/ProfileViewModel.cs +++ b/Source/NETworkManager/ViewModels/ProfileViewModel.cs @@ -308,11 +308,6 @@ public ProfileViewModel(Action saveCommand, Action x == profileInfo.SNMP_PrivacyProvider); SNMP_Priv = profileInfo.SNMP_Priv; - // Firewall - Firewall_Enabled = editMode == ProfileEditMode.Add - ? applicationName == ApplicationName.Firewall - : profileInfo.Firewall_Enabled; - // Wake on LAN WakeOnLAN_Enabled = editMode == ProfileEditMode.Add ? applicationName == ApplicationName.WakeOnLAN @@ -2755,24 +2750,6 @@ public SecureString SNMP_Priv #endregion - #region Firewall - - public bool Firewall_Enabled - { - get; - set - { - if (value == field) - return; - - field = value; - OnPropertyChanged(); - OnPropertyChanged(nameof(Name)); - } - } - - #endregion Firewall - #region Wake on LAN public bool WakeOnLAN_Enabled diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md index 2df41043a1..839fd794a1 100644 --- a/Website/docs/changelog/next-release.md +++ b/Website/docs/changelog/next-release.md @@ -27,6 +27,8 @@ Release date: **xx.xx.2026** ## Improvements +- The collapsed/expanded state of profile groups (e.g. **linux-server**) is now remembered per profile file and shared across all tools, instead of resetting every time you switch tools or restart the application. [#3539](https://github.com/BornToBeRoot/NETworkManager/pull/3539) + ## Bug Fixes ## Dependencies, Refactoring & Documentation