Skip to content

Commit

Permalink
Normalization doesn't move the viewport (AnnoDesigner#394)
Browse files Browse the repository at this point in the history
- LayoutVersion is loaded when layout is loaded
- Change of layout version is undoable and therefor counts toward unsaved changes
- Star is shown in the window title when there are unsaved changes
  • Loading branch information
Atria1234 committed Dec 23, 2021
1 parent 4d9336e commit be663b6
Show file tree
Hide file tree
Showing 12 changed files with 269 additions and 8 deletions.
31 changes: 31 additions & 0 deletions AnnoDesigner.Core/Converters/UnsavedChangesConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace AnnoDesigner.Core.Converters
{
public sealed class UnsavedChangesConverter : IMultiValueConverter
{
public string Format { get; set; } = "{0} *";

public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length < 1)
{
return DependencyProperty.UnsetValue;
}
if (values.Length < 2 || values[1] is false)
{
return values[0];
}

return string.Format(Format, values[0] as string);
}

public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
24 changes: 24 additions & 0 deletions AnnoDesigner.Core/Models/INotifyPropertyChangedWithValues.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.ComponentModel;

namespace AnnoDesigner.Core.Models
{
public class PropertyChangedWithValuesEventArgs<T> : PropertyChangedEventArgs
{
public T OldValue { get; set; }

public T NewValue { get; set; }

public PropertyChangedWithValuesEventArgs(string propertyName, T oldValue, T newValue) : base(propertyName)
{
OldValue = oldValue;
NewValue = newValue;
}
}

public delegate void PropertyChangedWithValuesEventHandler<T>(object sender, PropertyChangedWithValuesEventArgs<T> e);

public interface INotifyPropertyChangedWithValues<T>
{
event PropertyChangedWithValuesEventHandler<T> PropertyChangedWithValues;
}
}
11 changes: 10 additions & 1 deletion AnnoDesigner.Core/Models/Notify.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,31 @@ namespace AnnoDesigner.Core.Models
/// Holds the base INotifyPropertyChanged implementation plus helper methods
/// //https://stackoverflow.com/questions/1315621/implementing-inotifypropertychanged-does-a-better-way-exist
/// </summary>
public class Notify : INotifyPropertyChanged
public class Notify : INotifyPropertyChanged, INotifyPropertyChangedWithValues<object>
{
public event PropertyChangedEventHandler PropertyChanged;
public event PropertyChangedWithValuesEventHandler<object> PropertyChangedWithValues;

protected void OnPropertyChanged(string propertyName)
{
//Invoke event if not null
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

protected void OnPropertyChangedWithValues(string propertyName, object oldValue, object newValue)
{
//Invoke event if not null
PropertyChangedWithValues?.Invoke(this, new PropertyChangedWithValuesEventArgs<object>(propertyName, oldValue, newValue));
}

protected bool UpdateProperty<T>(ref T field, T value, [CallerMemberName] string name = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
{
return false;
}

OnPropertyChangedWithValues(name, field, value);
field = value;
OnPropertyChanged(name);
return true;
Expand Down
15 changes: 10 additions & 5 deletions AnnoDesigner/AnnoCanvas.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2661,9 +2661,6 @@ public void Normalize()
/// <param name="border"></param>
public void Normalize(int border)
{
_viewport.Left = 0;
_viewport.Top = 0;

if (PlacedObjects.Count == 0)
{
return;
Expand Down Expand Up @@ -2692,6 +2689,15 @@ public void Normalize(int border)
InvalidateScroll();
}

/// <summary>
/// Resets viewport of the canvas to top left corner.
/// </summary>
public void ResetViewport()
{
_viewport.Top = 0;
_viewport.Left = 0;
}

/// <summary>
/// Registers hotkeys with the <see cref="HotkeyCommandManager"/>.
/// </summary>
Expand Down Expand Up @@ -2767,8 +2773,7 @@ public void NewFile()
return;
}

_viewport.Left = 0;
_viewport.Top = 0;
ResetViewport();
PlacedObjects.Clear();
SelectedObjects.Clear();
UndoManager.Clear();
Expand Down
8 changes: 7 additions & 1 deletion AnnoDesigner/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
d:DataContext="{d:DesignInstance Type=viewModel:MainViewModel, IsDesignTimeCreatable=False}"
d:DesignHeight="480"
d:DesignWidth="640"
Title="{Binding MainWindowTitle}"
Icon="/AnnoDesigner;component/icon.ico"
Height="{Binding MainWindowHeight, Mode=TwoWay}"
Width="{Binding MainWindowWidth, Mode=TwoWay}"
Expand All @@ -31,6 +30,7 @@
Loaded="WindowLoaded"
Closing="WindowClosing">
<Window.Resources>
<coreConverters:UnsavedChangesConverter x:Key="UnsavedChangesConverter"/>
<coreConverters:BoolToVisibilityConverter x:Key="converterBoolToVisibility"
TrueValue="Visible"
FalseValue="Hidden" />
Expand All @@ -42,6 +42,12 @@
<TextBlock Text="{Binding Label}" />
</HierarchicalDataTemplate>
</Window.Resources>
<Window.Title>
<MultiBinding Converter="{StaticResource UnsavedChangesConverter}">
<Binding Path="MainWindowTitle" />
<Binding Path="AnnoCanvas.UndoManager.IsDirty" />
</MultiBinding>
</Window.Title>
<xctk:BusyIndicator x:Name="busyIndicator">
<Grid>
<Grid.RowDefinitions>
Expand Down
1 change: 1 addition & 0 deletions AnnoDesigner/Models/IAnnoCanvas.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public interface IAnnoCanvas : IHotkeySource
void ResetZoom();
void Normalize();
void Normalize(int border);
void ResetViewport();
void RaiseStatisticsUpdated(UpdateStatisticsEventArgs args);
void RaiseColorsInLayoutUpdated();
Rect ComputeBoundingRect(IEnumerable<LayoutObject> objects);
Expand Down
24 changes: 24 additions & 0 deletions AnnoDesigner/Undo/Operations/ModifyLayoutVersionOperation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using AnnoDesigner.ViewModels;

namespace AnnoDesigner.Undo.Operations
{
public class ModifyLayoutVersionOperation : BaseOperation
{
public LayoutSettingsViewModel LayoutSettingsViewModel { get; set; }

public Version OldValue { get; set; }

public Version NewValue { get; set; }

protected override void UndoOperation()
{
LayoutSettingsViewModel.LayoutVersion = OldValue;
}

protected override void RedoOperation()
{
LayoutSettingsViewModel.LayoutVersion = NewValue;
}
}
}
17 changes: 16 additions & 1 deletion AnnoDesigner/Undo/UndoManager.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using System;
using System.Collections.Generic;
using AnnoDesigner.Core.Models;
using AnnoDesigner.Undo.Operations;

namespace AnnoDesigner.Undo
{
public class UndoManager : IUndoManager
public class UndoManager : Notify, IUndoManager
{
internal Stack<IOperation> UndoStack { get; set; } = new Stack<IOperation>();
internal Stack<IOperation> RedoStack { get; set; } = new Stack<IOperation>();
Expand All @@ -23,26 +24,37 @@ public bool IsDirty
}

lastUndoableOperation = UndoStack.Count > 0 ? UndoStack.Peek() : null;
OnPropertyChanged(nameof(IsDirty));
}
}

private bool Undoing { get; set; }

public void Undo()
{
if (UndoStack.Count > 0)
{
var wasUndoing = Undoing;
Undoing = true;
var operation = UndoStack.Pop();
operation.Undo();
RedoStack.Push(operation);
OnPropertyChanged(nameof(IsDirty));
Undoing = wasUndoing;
}
}

public void Redo()
{
if (RedoStack.Count > 0)
{
var wasUndoing = Undoing;
Undoing = true;
var operation = RedoStack.Pop();
operation.Redo();
UndoStack.Push(operation);
OnPropertyChanged(nameof(IsDirty));
Undoing = wasUndoing;
}
}

Expand All @@ -55,6 +67,8 @@ public void Clear()

public void RegisterOperation(IOperation operation)
{
if (Undoing) return;

if (CurrentCompositeOperation != null)
{
CurrentCompositeOperation.Operations.Add(operation);
Expand All @@ -63,6 +77,7 @@ public void RegisterOperation(IOperation operation)
{
UndoStack.Push(operation);
RedoStack.Clear();
OnPropertyChanged(nameof(IsDirty));
}
}

Expand Down
17 changes: 17 additions & 0 deletions AnnoDesigner/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ public MainViewModel(ICommons commonsToUse,
PreferencesGeneralViewModel = new GeneralSettingsViewModel(_appSettings, _commons, _recentFilesHelper);

LayoutSettingsViewModel = new LayoutSettingsViewModel();
LayoutSettingsViewModel.PropertyChangedWithValues += LayoutSettingsViewModel_PropertyChangedWithValues;

OpenProjectHomepageCommand = new RelayCommand(OpenProjectHomepage);
CloseWindowCommand = new RelayCommand<ICloseable>(CloseWindow);
Expand Down Expand Up @@ -220,6 +221,19 @@ public MainViewModel(ICommons commonsToUse,
RecentFilesHelper_Updated(this, EventArgs.Empty);
}

private void LayoutSettingsViewModel_PropertyChangedWithValues(object sender, PropertyChangedWithValuesEventArgs<object> e)
{
if (e.PropertyName == nameof(LayoutSettingsViewModel.LayoutVersion))
{
AnnoCanvas.UndoManager.RegisterOperation(new ModifyLayoutVersionOperation()
{
LayoutSettingsViewModel = sender as LayoutSettingsViewModel,
OldValue = e.OldValue as Version,
NewValue = e.NewValue as Version,
});
}
}

private IconImage GenerateNoIconItem()
{
var localizations = new Dictionary<string, string>();
Expand Down Expand Up @@ -798,6 +812,7 @@ public void OpenLayout(LayoutFile layout)
AnnoCanvas.PlacedObjects.AddRange(layoutObjects);

AnnoCanvas.Normalize(1);
AnnoCanvas.ResetViewport();

AnnoCanvas.RaiseStatisticsUpdated(UpdateStatisticsEventArgs.All);
AnnoCanvas.RaiseColorsInLayoutUpdated();
Expand Down Expand Up @@ -1113,6 +1128,7 @@ private void CanvasResetZoom(object param)
private void CanvasNormalize(object param)
{
AnnoCanvas.Normalize(1);
AnnoCanvas.ResetViewport();
}

public ICommand MergeRoadsCommand { get; private set; }
Expand Down Expand Up @@ -1213,6 +1229,7 @@ private void ExecuteLoadLayoutFromJsonSub(string jsonString, bool forceLoad = fa

AnnoCanvas.LoadedFile = string.Empty;
AnnoCanvas.Normalize(1);
AnnoCanvas.ResetViewport();

_ = UpdateStatisticsAsync(UpdateMode.All);
}
Expand Down
26 changes: 26 additions & 0 deletions Tests/AnnoDesigner.Tests/MainViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
using AnnoDesigner.Core.RecentFiles;
using AnnoDesigner.Core.Services;
using AnnoDesigner.Models;
using AnnoDesigner.Undo;
using AnnoDesigner.Undo.Operations;
using AnnoDesigner.ViewModels;
using Moq;
using Xunit;
Expand Down Expand Up @@ -54,6 +56,7 @@ public MainViewModelTests()
var annoCanvasMock = new Mock<IAnnoCanvas>();
annoCanvasMock.SetupAllProperties();
//The QuadTree does not have a default constructor, so we need to explicitly set up the property
annoCanvasMock.SetupGet(x => x.UndoManager).Returns(Mock.Of<IUndoManager>());
annoCanvasMock.Setup(x => x.PlacedObjects).Returns(new Core.DataStructures.QuadTree<LayoutObject>(new Rect(-100, -100, 200, 200)));
_mockedAnnoCanvas = annoCanvasMock.Object;

Expand Down Expand Up @@ -1115,5 +1118,28 @@ public void SaveFile_ExceptionIsRaised_ShouldShowError()
}

#endregion

#region

[Fact]
public void LayoutSettingsViewModel_SettingLayoutVersion_ShouldBeConsideredUnsavedChange()
{
// Arrange
var versionToSave = new Version(42, 42, 42, 42);

var mockedAnnoCanvas = new Mock<IAnnoCanvas>();
var mockedUndoManager = new Mock<IUndoManager>();
mockedAnnoCanvas.SetupAllProperties();
mockedAnnoCanvas.SetupGet(x => x.UndoManager).Returns(mockedUndoManager.Object);
var viewModel = GetViewModel(annoCanvasToUse: mockedAnnoCanvas.Object);

// Act
viewModel.LayoutSettingsViewModel.LayoutVersion = versionToSave;

// Assert
mockedUndoManager.Verify(x => x.RegisterOperation(It.Is<ModifyLayoutVersionOperation>(y => y.NewValue == new Version(42, 42, 42, 42))));
}

#endregion
}
}
Loading

0 comments on commit be663b6

Please sign in to comment.