diff --git a/AnnoDesigner.Core/Converters/FlagToBoolConverter.cs b/AnnoDesigner.Core/Converters/FlagToBoolConverter.cs new file mode 100644 index 00000000..6b7cd1ef --- /dev/null +++ b/AnnoDesigner.Core/Converters/FlagToBoolConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace AnnoDesigner.Core.Converters +{ + [ValueConversion(typeof(Enum), typeof(bool))] + public sealed class FlagToBoolConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is Enum e && parameter is Enum p) + { + return e.HasFlag(p); + } + return DependencyProperty.UnsetValue; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/AnnoDesigner/MainWindow.xaml b/AnnoDesigner/MainWindow.xaml index 88e812f0..ae377c1e 100644 --- a/AnnoDesigner/MainWindow.xaml +++ b/AnnoDesigner/MainWindow.xaml @@ -37,6 +37,7 @@ + @@ -334,52 +335,6 @@ - - @@ -639,6 +594,69 @@ Command="{Binding PlaceBuildingCommand}" Height="23" TabIndex="12" /> + + + + + + + + + + + + + + + + + + + diff --git a/AnnoDesigner/MainWindow.xaml.cs b/AnnoDesigner/MainWindow.xaml.cs index c4aa0d47..2b274ee2 100644 --- a/AnnoDesigner/MainWindow.xaml.cs +++ b/AnnoDesigner/MainWindow.xaml.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Configuration; using System.Windows; +using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; @@ -146,5 +147,15 @@ private void WindowClosing(object sender, CancelEventArgs e) logger.Trace($"saving settings: \"{userConfig}\""); #endif } + + private void BuildingSettings_ApplySettings_Checked(object sender, RoutedEventArgs e) + { + DataContext.BuildingSettingsViewModel.ApplySettings |= (ApplySettings)(sender as CheckBox).Tag; + } + + private void BuildingSettings_ApplySettings_Unchecked(object sender, RoutedEventArgs e) + { + DataContext.BuildingSettingsViewModel.ApplySettings &= ~(ApplySettings)(sender as CheckBox).Tag; + } } } \ No newline at end of file diff --git a/AnnoDesigner/Models/LayoutObject.cs b/AnnoDesigner/Models/LayoutObject.cs index d041c4db..061626a5 100644 --- a/AnnoDesigner/Models/LayoutObject.cs +++ b/AnnoDesigner/Models/LayoutObject.cs @@ -591,5 +591,11 @@ public Rect GridInfluenceRangeRect return _gridInfluenceRangeRect; } } + + public string Label { get => WrappedAnnoObject.Label; set => WrappedAnnoObject.Label = value; } + public double InfluenceRange { get => WrappedAnnoObject.InfluenceRange; set => WrappedAnnoObject.InfluenceRange = value; } + public double Radius { get => WrappedAnnoObject.Radius; set => WrappedAnnoObject.Radius = value; } + public bool Borderless { get => WrappedAnnoObject.Borderless; set => WrappedAnnoObject.Borderless = value; } + public bool Road { get => WrappedAnnoObject.Road; set => WrappedAnnoObject.Road = value; } } } diff --git a/AnnoDesigner/Undo/Operations/CompositeOperation.cs b/AnnoDesigner/Undo/Operations/CompositeOperation.cs index 19171a73..a94b1874 100644 --- a/AnnoDesigner/Undo/Operations/CompositeOperation.cs +++ b/AnnoDesigner/Undo/Operations/CompositeOperation.cs @@ -1,11 +1,63 @@ -using System.Collections.Generic; +using System.Collections; +using System.Collections.Generic; using System.Linq; namespace AnnoDesigner.Undo.Operations { - public class CompositeOperation : BaseOperation + public class CompositeOperation : BaseOperation, ICollection { - public ICollection Operations { get; set; } = new List(); + private ICollection operations = new List(); + + public IEnumerable Operations => operations; + + public int Count => operations.Count; + + public bool IsReadOnly => operations.IsReadOnly; + + public CompositeOperation() + { + operations = new List(); + } + + public CompositeOperation(IEnumerable ops) + { + operations = new List(ops); + } + + public void Add(IOperation item) + { + operations.Add(item); + } + + public void Clear() + { + operations.Clear(); + } + + public bool Contains(IOperation item) + { + return operations.Contains(item); + } + + public void CopyTo(IOperation[] array, int arrayIndex) + { + operations.CopyTo(array, arrayIndex); + } + + public IEnumerator GetEnumerator() + { + return operations.GetEnumerator(); + } + + public bool Remove(IOperation item) + { + return operations.Remove(item); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)operations).GetEnumerator(); + } protected override void RedoOperation() { diff --git a/AnnoDesigner/Undo/UndoManager.cs b/AnnoDesigner/Undo/UndoManager.cs index bf18bac5..934f2237 100644 --- a/AnnoDesigner/Undo/UndoManager.cs +++ b/AnnoDesigner/Undo/UndoManager.cs @@ -57,7 +57,7 @@ public void RegisterOperation(IOperation operation) { if (CurrentCompositeOperation != null) { - CurrentCompositeOperation.Operations.Add(operation); + CurrentCompositeOperation.Add(operation); } else { @@ -78,7 +78,7 @@ public void AsSingleUndoableOperation(Action action) finally { CurrentCompositeOperation = null; - if (operation != null && operation.Operations.Count > 0) + if (operation != null && operation.Count > 0) { RegisterOperation(operation); } diff --git a/AnnoDesigner/ViewModels/BuildingSettingsViewModel.cs b/AnnoDesigner/ViewModels/BuildingSettingsViewModel.cs index 835fee23..df6856c6 100644 --- a/AnnoDesigner/ViewModels/BuildingSettingsViewModel.cs +++ b/AnnoDesigner/ViewModels/BuildingSettingsViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Windows.Input; @@ -14,9 +15,55 @@ namespace AnnoDesigner.ViewModels { + [Flags] + public enum ApplySettings + { + None = 0, + Color = 1 << 0, + Label = 1 << 1, + Icon = 1 << 2, + Influence = 1 << 3, + Borderless = 1 << 4, + Road = 1 << 5, + } + + public interface Aaa + { + ApplySettings ApplySettings { get; } + + IOperation SetValueAndGetUndoableOperation(IEnumerable objs); + } + + public class Aaaa : Aaa + { + public ApplySettings ApplySettings { get; set; } + + public string PropertyName { get; set; } + + public Func OldValueGetter { get; set; } + + public Func NewValueGetter { get; set; } + + public IOperation SetValueAndGetUndoableOperation(IEnumerable objs) + { + var operation = new ModifyObjectPropertiesOperation() + { + PropertyName = PropertyName, + ObjectPropertyValues = objs + .Select(obj => (obj, OldValueGetter(obj), NewValueGetter())) + .ToList() + }; + + operation.Redo(); + + return operation; + } + } + public class BuildingSettingsViewModel : Notify { private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + private readonly List applySettingsTemplates; private readonly IAppSettings _appSettings; private readonly IMessageBoxService _messageBoxService; @@ -38,8 +85,11 @@ public class BuildingSettingsViewModel : Notify private bool _isEnableLabelChecked; private bool _isBorderlessChecked; private bool _isRoadChecked; + private ApplySettings _applySettings = ApplySettings.Color | ApplySettings.Borderless; private ObservableCollection _colorsInLayout; private IAnnoCanvas _annoCanvasToUse; + private ICollection _availableIcons; + private IconImage _selectedIcon; private ColorHueSaturationBrightnessComparer _colorSorter; private ObservableCollection _buildingInfluences; private BuildingInfluence _selectedBuildingInfluence; @@ -51,14 +101,17 @@ public class BuildingSettingsViewModel : Notify /// public BuildingSettingsViewModel(IAppSettings appSettingsToUse, IMessageBoxService messageBoxServiceToUse, - ILocalizationHelper localizationHelperToUse) + ILocalizationHelper localizationHelperToUse, + ICollection availableIcons) { _appSettings = appSettingsToUse; _messageBoxService = messageBoxServiceToUse; _localizationHelper = localizationHelperToUse; + _availableIcons = availableIcons; - ApplyColorToSelectionCommand = new RelayCommand(ApplyColorToSelection, CanApplyColorToSelection); - ApplyPredefinedColorToSelectionCommand = new RelayCommand(ApplyPredefinedColorToSelection, CanApplyPredefinedColorToSelection); + ApplyColorToSelectionCommand = new RelayCommand(ApplyColorToSelection, AreSomeObjectsSelected); + ApplyPredefinedColorToSelectionCommand = new RelayCommand(ApplyPredefinedColorToSelection, AreSomeObjectsSelected); + ApplySettingsToSelectionCommand = new RelayCommand(ApplySettingsToSelection, AreSomeObjectsSelected); UseColorInLayoutCommand = new RelayCommand(UseColorInLayout, CanUseColorInLayout); SelectedColor = Colors.Red; @@ -77,6 +130,59 @@ public BuildingSettingsViewModel(IAppSettings appSettingsToUse, BuildingInfluences = new ObservableCollection(); InitBuildingInfluences(); SelectedBuildingInfluence = BuildingInfluences.SingleOrDefault(x => x.Type == BuildingInfluenceType.None); + + applySettingsTemplates = new() + { + new Aaaa() + { + ApplySettings = ApplySettings.Color, + PropertyName = nameof(LayoutObject.Color), + OldValueGetter = x => x.Color, + NewValueGetter = () => SelectedColor.Value + }, + new Aaaa() + { + ApplySettings = ApplySettings.Label, + PropertyName = nameof(LayoutObject.Label), + OldValueGetter = x => x.WrappedAnnoObject.Label, + NewValueGetter = () => BuildingName + }, + new Aaaa() + { + ApplySettings = ApplySettings.Icon, + PropertyName = nameof(LayoutObject.Icon), + OldValueGetter = x => x.Icon, + NewValueGetter = () => SelectedIcon + }, + new Aaaa() + { + ApplySettings = ApplySettings.Influence, + PropertyName = nameof(LayoutObject.InfluenceRange), + OldValueGetter = x => x.WrappedAnnoObject.InfluenceRange, + NewValueGetter = () => BuildingInfluenceRange + }, + new Aaaa() + { + ApplySettings = ApplySettings.Influence, + PropertyName = nameof(LayoutObject.Radius), + OldValueGetter = x => x.WrappedAnnoObject.Radius, + NewValueGetter = () => BuildingRadius + }, + new Aaaa() + { + ApplySettings = ApplySettings.Borderless, + PropertyName = nameof(LayoutObject.Borderless), + OldValueGetter = x => x.WrappedAnnoObject.Borderless, + NewValueGetter = () => IsBorderlessChecked + }, + new Aaaa() + { + ApplySettings = ApplySettings.Road, + PropertyName = nameof(LayoutObject.Road), + OldValueGetter = x => x.WrappedAnnoObject.Road, + NewValueGetter = () => IsRoadChecked + }, + }; } private void InitBuildingInfluences() @@ -109,6 +215,18 @@ public Color? SelectedColor set { UpdateProperty(ref _selectedColor, value); } } + public ICollection AvailableIcons + { + get { return _availableIcons; } + set { UpdateProperty(ref _availableIcons, value); } + } + + public IconImage SelectedIcon + { + get { return _selectedIcon; } + set { UpdateProperty(ref _selectedIcon, value); } + } + public int BuildingHeight { get { return _buildingHeight; } @@ -205,6 +323,12 @@ public bool IsRoadChecked set { UpdateProperty(ref _isRoadChecked, value); } } + public ApplySettings ApplySettings + { + get { return _applySettings; } + set { UpdateProperty(ref _applySettings, value); } + } + public IAnnoCanvas AnnoCanvasToUse { get { return _annoCanvasToUse; } @@ -237,7 +361,7 @@ public ObservableCollection ColorsInLayout private ColorHueSaturationBrightnessComparer ColorSorter { - get { return _colorSorter ?? (_colorSorter = new ColorHueSaturationBrightnessComparer()); } + get { return _colorSorter ??= new ColorHueSaturationBrightnessComparer(); } } public ObservableCollection BuildingInfluences @@ -382,7 +506,7 @@ private void ApplyColorToSelection(object param) ObjectPropertyValues = AnnoCanvasToUse.SelectedObjects .Select(obj => (obj, obj.Color, selectedColor: (SerializableColor)SelectedColor.Value)) .ToList(), - AfterAction = ColorChangeUndone + AfterAction = RerenderCanvas }); foreach (var curSelectedObject in AnnoCanvasToUse.SelectedObjects) @@ -395,11 +519,6 @@ private void ApplyColorToSelection(object param) AnnoCanvasToUse_ColorsUpdated(this, EventArgs.Empty); } - private bool CanApplyColorToSelection(object param) - { - return AnnoCanvasToUse?.SelectedObjects.Count > 0; - } - public ICommand ApplyPredefinedColorToSelectionCommand { get; private set; } private void ApplyPredefinedColorToSelection(object param) @@ -416,7 +535,7 @@ private void ApplyPredefinedColorToSelection(object param) .Where(obj => ColorPresetsHelper.Instance.GetPredefinedColor(obj.WrappedAnnoObject).HasValue) .Select(obj => (obj, obj.Color, (SerializableColor)ColorPresetsHelper.Instance.GetPredefinedColor(obj.WrappedAnnoObject).Value)) .ToList(), - AfterAction = ColorChangeUndone + AfterAction = RerenderCanvas }); foreach (var curSelectedObject in AnnoCanvasToUse.SelectedObjects) @@ -428,11 +547,32 @@ private void ApplyPredefinedColorToSelection(object param) } } - AnnoCanvasToUse.ForceRendering(); - AnnoCanvasToUse_ColorsUpdated(this, EventArgs.Empty); + RerenderCanvas(); + } + + public ICommand ApplySettingsToSelectionCommand { get; private set; } + + private void ApplySettingsToSelection(object param) + { + if (AnnoCanvasToUse == null) + { + return; + } + + var operations = applySettingsTemplates.Where(x => ApplySettings.HasFlag(x.ApplySettings)).Select(x => x.SetValueAndGetUndoableOperation(AnnoCanvasToUse.SelectedObjects)).ToList(); + + if (operations.Count > 0) + { + AnnoCanvasToUse.UndoManager.RegisterOperation(new CompositeOperation(operations) + { + AfterAction = RerenderCanvas + }); + + RerenderCanvas(); + } } - private bool CanApplyPredefinedColorToSelection(object param) + private bool AreSomeObjectsSelected(object param) { return AnnoCanvasToUse?.SelectedObjects.Count > 0; } @@ -477,7 +617,7 @@ private void AnnoCanvasToUse_ColorsUpdated(object sender, EventArgs e) OnPropertyChanged(nameof(ShowColorsInLayout)); } - private void ColorChangeUndone() + private void RerenderCanvas() { AnnoCanvasToUse.ForceRendering(); AnnoCanvasToUse_ColorsUpdated(this, EventArgs.Empty); diff --git a/AnnoDesigner/ViewModels/MainViewModel.cs b/AnnoDesigner/ViewModels/MainViewModel.cs index 827efdfb..36008ff5 100644 --- a/AnnoDesigner/ViewModels/MainViewModel.cs +++ b/AnnoDesigner/ViewModels/MainViewModel.cs @@ -126,7 +126,11 @@ public MainViewModel(ICommons commonsToUse, StatisticsViewModel.IsVisible = _appSettings.StatsShowStats; StatisticsViewModel.ShowStatisticsBuildingCount = _appSettings.StatsShowBuildingCount; - BuildingSettingsViewModel = new BuildingSettingsViewModel(_appSettings, _messageBoxService, _localizationHelper); + AvailableIcons = new ObservableCollection(); + _noIconItem = GenerateNoIconItem(); + AvailableIcons.Add(_noIconItem); + + BuildingSettingsViewModel = new BuildingSettingsViewModel(_appSettings, _messageBoxService, _localizationHelper, AvailableIcons); // load tree localization try @@ -175,11 +179,6 @@ public MainViewModel(ICommons commonsToUse, ShowLicensesWindowCommand = new RelayCommand(ExecuteShowLicensesWindow); OpenRecentFileCommand = new RelayCommand(ExecuteOpenRecentFile); - AvailableIcons = new ObservableCollection(); - _noIconItem = GenerateNoIconItem(); - AvailableIcons.Add(_noIconItem); - SelectedIcon = _noIconItem; - RecentFiles = new ObservableCollection(); _recentFilesHelper.Updated += RecentFilesHelper_Updated; @@ -265,7 +264,7 @@ private void Commons_SelectedLanguageChanged(object sender, EventArgs e) AvailableIcons.Clear(); AvailableIcons.Add(_noIconItem); LoadAvailableIcons(); - SelectedIcon = _noIconItem; + BuildingSettingsViewModel.SelectedIcon = _noIconItem; } catch (Exception ex) { @@ -356,7 +355,7 @@ private void ApplyCurrentObject() Size = new Size(BuildingSettingsViewModel.BuildingWidth, BuildingSettingsViewModel.BuildingHeight), Color = BuildingSettingsViewModel.SelectedColor ?? Colors.Red, Label = BuildingSettingsViewModel.IsEnableLabelChecked ? BuildingSettingsViewModel.BuildingName : string.Empty, - Icon = SelectedIcon == _noIconItem ? null : SelectedIcon.Name, + Icon = BuildingSettingsViewModel.SelectedIcon == _noIconItem ? null : BuildingSettingsViewModel.SelectedIcon.Name, Radius = BuildingSettingsViewModel.BuildingRadius, InfluenceRange = BuildingSettingsViewModel.BuildingInfluenceRange, PavedStreet = BuildingSettingsViewModel.IsPavedStreet, @@ -459,19 +458,19 @@ private void UpdateUIFromObject(LayoutObject layoutObject) { if (string.IsNullOrWhiteSpace(obj.Icon)) { - SelectedIcon = _noIconItem; + BuildingSettingsViewModel.SelectedIcon = _noIconItem; } else { var foundIconImage = AvailableIcons.SingleOrDefault(x => x.Name.Equals(Path.GetFileNameWithoutExtension(obj.Icon), StringComparison.OrdinalIgnoreCase)); - SelectedIcon = foundIconImage ?? _noIconItem; + BuildingSettingsViewModel.SelectedIcon = foundIconImage ?? _noIconItem; } } catch (Exception ex) { Console.WriteLine($"Error finding {nameof(IconImage)} for value \"{obj.Icon}\".{Environment.NewLine}{ex}"); - SelectedIcon = _noIconItem; + BuildingSettingsViewModel.SelectedIcon = _noIconItem; } // radius @@ -1002,12 +1001,6 @@ public ObservableCollection AvailableIcons set { UpdateProperty(ref _availableIcons, value); } } - public IconImage SelectedIcon - { - get { return _selectedIcon; } - set { UpdateProperty(ref _selectedIcon, value); } - } - public string MainWindowTitle { get { return _mainWindowTitle; } diff --git a/Tests/AnnoDesigner.Tests/BuildingSettingsViewModelTests.cs b/Tests/AnnoDesigner.Tests/BuildingSettingsViewModelTests.cs index 2108c265..d614450c 100644 --- a/Tests/AnnoDesigner.Tests/BuildingSettingsViewModelTests.cs +++ b/Tests/AnnoDesigner.Tests/BuildingSettingsViewModelTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Windows.Media; @@ -41,7 +42,8 @@ private BuildingSettingsViewModel GetViewModel(IAppSettings appSettingsToUse = n { return new BuildingSettingsViewModel(appSettingsToUse ?? _mockedAppSettings, _mockedMessageBoxService, - _mockedLocalization); + _mockedLocalization, + new List()); } #region ctor tests diff --git a/Tests/AnnoDesigner.Tests/MainViewModelTests.cs b/Tests/AnnoDesigner.Tests/MainViewModelTests.cs index c4d9c47c..a1fa730e 100644 --- a/Tests/AnnoDesigner.Tests/MainViewModelTests.cs +++ b/Tests/AnnoDesigner.Tests/MainViewModelTests.cs @@ -136,7 +136,6 @@ public void Ctor_ShouldSetDefaultValues() Assert.Null(viewModel.StatusMessageClipboard); Assert.NotNull(viewModel.AvailableIcons); - Assert.NotNull(viewModel.SelectedIcon); Assert.NotNull(viewModel.Languages); Assert.NotNull(viewModel.MainWindowTitle); Assert.NotNull(viewModel.PresetsSectionHeader); diff --git a/Tests/AnnoDesigner.Tests/Undo/CompositeOperationTests.cs b/Tests/AnnoDesigner.Tests/Undo/CompositeOperationTests.cs index 8c82a854..f28c2c27 100644 --- a/Tests/AnnoDesigner.Tests/Undo/CompositeOperationTests.cs +++ b/Tests/AnnoDesigner.Tests/Undo/CompositeOperationTests.cs @@ -20,14 +20,11 @@ public void Undo_ShouldUndoOperationsInCorrectOrder() var op2 = new Mock(); _ = op2.Setup(op => op.Undo()).Callback(() => order.Add(op2.Object)); - var operation = new CompositeOperation() + var operation = new CompositeOperation(new List() { - Operations = new List() - { - op1.Object, - op2.Object - } - }; + op1.Object, + op2.Object + }); // Act operation.Undo(); @@ -51,14 +48,11 @@ public void Redo_ShouldRedoOperationsInCorrectOrder() var op2 = new Mock(); _ = op2.Setup(op => op.Redo()).Callback(() => order.Add(op2.Object)); - var operation = new CompositeOperation() + var operation = new CompositeOperation(new List() { - Operations = new List() - { - op1.Object, - op2.Object - } - }; + op1.Object, + op2.Object + }); // Act operation.Redo();