diff --git a/Gen3Hex/Gen3Hex.csproj b/Gen3Hex/Gen3Hex.csproj
index 377bcc49..a1eaa2cf 100644
--- a/Gen3Hex/Gen3Hex.csproj
+++ b/Gen3Hex/Gen3Hex.csproj
@@ -65,6 +65,7 @@
+
MSBuild:Compile
Designer
diff --git a/Gen3Hex/View/App.xaml.cs b/Gen3Hex/View/App.xaml.cs
index 27470f34..01ecb305 100644
--- a/Gen3Hex/View/App.xaml.cs
+++ b/Gen3Hex/View/App.xaml.cs
@@ -11,17 +11,19 @@ namespace HavenSoft.Gen3Hex.View {
protected override void OnStartup(StartupEventArgs e) {
base.OnStartup(e);
var fileName = e.Args?.Length == 1 ? e.Args[0] : string.Empty;
- var viewPort = GetViewPort(fileName);
+ var viewPort = GetViewModel(fileName);
MainWindow = new MainWindow(viewPort);
MainWindow.Show();
}
- private ViewPort GetViewPort(string fileName) {
- if (!File.Exists(fileName)) return new ViewPort();
+ private EditorViewModel GetViewModel(string fileName) {
+ var editor = new EditorViewModel(new WindowsFileSystem());
+ if (!File.Exists(fileName)) return editor;
var bytes = File.ReadAllBytes(fileName);
var loadedFile = new LoadedFile(fileName, bytes);
- return new ViewPort(loadedFile);
+ editor.Add(new ViewPort(loadedFile));
+ return editor;
}
}
}
diff --git a/Gen3Hex/View/HexContent.cs b/Gen3Hex/View/HexContent.cs
index d0330782..ddb95639 100644
--- a/Gen3Hex/View/HexContent.cs
+++ b/Gen3Hex/View/HexContent.cs
@@ -1,5 +1,6 @@
using HavenSoft.Gen3Hex.Model;
using HavenSoft.Gen3Hex.ViewModel;
+using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
@@ -107,6 +108,11 @@ namespace HavenSoft.Gen3Hex.View {
ViewPort.SelectionEnd = ControlCoordinatesToModelCoordinates(e);
}
+ protected override void OnMouseWheel(MouseWheelEventArgs e) {
+ base.OnMouseWheel(e);
+ ViewPort.ScrollValue -= Math.Sign(e.Delta);
+ }
+
protected override void OnRender(DrawingContext drawingContext) {
base.OnRender(drawingContext);
if (ViewPort == null) return;
diff --git a/Gen3Hex/View/MainWindow.xaml b/Gen3Hex/View/MainWindow.xaml
index f32e4ab1..0bd20b33 100644
--- a/Gen3Hex/View/MainWindow.xaml
+++ b/Gen3Hex/View/MainWindow.xaml
@@ -4,10 +4,51 @@
xmlns:hsg3hv="clr-namespace:HavenSoft.Gen3Hex.View"
xmlns:solarized="clr-namespace:Solarized;assembly=HavenSoft"
Title="Gen3Hex" Height="450" Width="800" Background="{solarized:Theme Background}">
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Gen3Hex/View/MainWindow.xaml.cs b/Gen3Hex/View/MainWindow.xaml.cs
index 3a0e7843..439d09ca 100644
--- a/Gen3Hex/View/MainWindow.xaml.cs
+++ b/Gen3Hex/View/MainWindow.xaml.cs
@@ -1,20 +1,26 @@
using HavenSoft.Gen3Hex.ViewModel;
using System;
-using System.Windows.Input;
+using System.ComponentModel;
namespace HavenSoft.Gen3Hex.View {
public partial class MainWindow {
- private readonly ViewPort viewPort;
+ public EditorViewModel ViewModel { get; }
- public MainWindow(ViewPort viewModel) {
+ public MainWindow(EditorViewModel viewModel) {
InitializeComponent();
- viewPort = viewModel;
+ ViewModel = viewModel;
DataContext = viewModel;
}
- protected override void OnMouseWheel(MouseWheelEventArgs e) {
- base.OnMouseWheel(e);
- viewPort.ScrollValue -= Math.Sign(e.Delta);
+ protected override void OnClosing(CancelEventArgs e) {
+ base.OnClosing(e);
+ ViewModel.CloseAll.Execute();
+ if (ViewModel.Count != 0) e.Cancel = true;
+ }
+
+ private void ExitClicked(object sender, EventArgs e) {
+ ViewModel.CloseAll.Execute();
+ if (ViewModel.Count == 0) Close();
}
}
}
diff --git a/Gen3Hex/View/WindowsFileSystem.cs b/Gen3Hex/View/WindowsFileSystem.cs
new file mode 100644
index 00000000..36aa771c
--- /dev/null
+++ b/Gen3Hex/View/WindowsFileSystem.cs
@@ -0,0 +1,42 @@
+using HavenSoft.Gen3Hex.Model;
+using Microsoft.Win32;
+using System.IO;
+using System.Linq;
+using System.Windows;
+
+namespace HavenSoft.Gen3Hex.View {
+ public class WindowsFileSystem : IFileSystem {
+ public LoadedFile OpenFile(params string[] extensionOptions) {
+ var dialog = new OpenFileDialog { Filter = CreateFilterFromOptions(extensionOptions) };
+ var result = dialog.ShowDialog();
+ if (result != true) return null;
+ if (!File.Exists(dialog.FileName)) return null;
+ var data = File.ReadAllBytes(dialog.FileName);
+ return new LoadedFile(dialog.FileName, data);
+ }
+
+ public string RequestNewName(string currentName, params string[] extensionOptions) {
+ var dialog = new SaveFileDialog { FileName = currentName, Filter = CreateFilterFromOptions(extensionOptions) };
+ var result = dialog.ShowDialog();
+ if (result != true) return null;
+ return dialog.FileName;
+ }
+
+ public bool Save(LoadedFile file) {
+ if (!File.Exists(file.Name)) return false;
+ File.WriteAllBytes(file.Name, file.Contents);
+ return true;
+ }
+
+ public bool? TrySavePrompt(LoadedFile file) {
+ var result = MessageBox.Show($"Would you like to save {file.Name}?", Application.Current.MainWindow.Title, MessageBoxButton.YesNoCancel);
+ if (result == MessageBoxResult.Cancel) return null;
+ if (result == MessageBoxResult.No) return false;
+ return Save(file);
+ }
+
+ private static string CreateFilterFromOptions(string[] extensionOptions) {
+ return string.Join(",", extensionOptions.Select(option => $"*.{option}"));
+ }
+ }
+}
diff --git a/Gen3Hex/ViewModel/EditorViewModel.cs b/Gen3Hex/ViewModel/EditorViewModel.cs
index 9d6699de..189f0cdb 100644
--- a/Gen3Hex/ViewModel/EditorViewModel.cs
+++ b/Gen3Hex/ViewModel/EditorViewModel.cs
@@ -11,19 +11,19 @@ namespace HavenSoft.Gen3Hex.ViewModel {
private readonly IFileSystem fileSystem;
private readonly List tabs;
- private readonly StubCommand saveAll, closeAll, newCommand, open;
+ private readonly StubCommand newCommand, open, save, saveAs, saveAll, close, closeAll, undo, redo;
private int selectedIndex;
- public ICommand Undo => SelectedTab?.Undo;
- public ICommand Redo => SelectedTab?.Redo;
- public ICommand Save => SelectedTab?.Save;
- public ICommand SaveAs => SelectedTab?.SaveAs;
- public ICommand Close => SelectedTab?.Close;
- public ICommand SaveAll => saveAll;
- public ICommand CloseAll => closeAll;
- public ICommand Open => open;
public ICommand New => newCommand;
+ public ICommand Open => open;
+ public ICommand Save => save;
+ public ICommand SaveAs => saveAs;
+ public ICommand SaveAll => saveAll;
+ public ICommand Close => close;
+ public ICommand CloseAll => closeAll;
+ public ICommand Undo => undo;
+ public ICommand Redo => redo;
#region Collection Properties
@@ -36,15 +36,10 @@ namespace HavenSoft.Gen3Hex.ViewModel {
public int SelectedIndex {
get => selectedIndex;
set {
- if (!TryUpdate(ref selectedIndex, value)) return;
- var commandsToRefresh = new List {
- nameof(Undo),
- nameof(Redo),
- nameof(Save),
- nameof(SaveAs),
- nameof(Close),
- };
- commandsToRefresh.ForEach(NotifyPropertyChanged);
+ StopListeningToCommandsFromCurrentTab();
+ bool updated = TryUpdate(ref selectedIndex, value);
+ StartListeningToCommandsFromCurrentTab();
+ if (!updated) return;
}
}
@@ -57,17 +52,9 @@ namespace HavenSoft.Gen3Hex.ViewModel {
tabs = new List();
selectedIndex = -1;
- saveAll = new StubCommand {
- CanExecute = arg => tabs.Any(tab => tab.Save.CanExecute(fileSystem)),
- Execute = arg => tabs.ForEach(tab => {
- if (tab.Save.CanExecute(fileSystem)) {
- tab.Save.Execute(fileSystem);
- }
- }),
- };
- closeAll = new StubCommand {
- CanExecute = arg => tabs.Any(tab => tab.Close.CanExecute(fileSystem)),
- Execute = arg => tabs.ToList().ForEach(tab => tab.Close.Execute(fileSystem)), // ToList -> because closing a tab modifies the original list
+ newCommand = new StubCommand {
+ CanExecute = arg => true,
+ Execute = arg => Add(new ViewPort()),
};
open = new StubCommand {
CanExecute = arg => true,
@@ -79,10 +66,27 @@ namespace HavenSoft.Gen3Hex.ViewModel {
Add(tab);
},
};
- newCommand = new StubCommand {
- CanExecute = arg => true,
- Execute = arg => Add(new ViewPort()),
+ save = CreateWrapperFor(tab => tab.Save);
+ saveAs = CreateWrapperFor(tab => tab.SaveAs);
+ saveAll = new StubCommand {
+ CanExecute = arg => tabs.Any(tab => tab.Save.CanExecute(fileSystem)),
+ Execute = arg => tabs.ForEach(tab => {
+ if (tab.Save.CanExecute(fileSystem)) {
+ tab.Save.Execute(fileSystem);
+ }
+ }),
};
+ close = CreateWrapperFor(tab => tab.Close);
+ closeAll = new StubCommand {
+ CanExecute = arg => tabs.Any(tab => tab.Close.CanExecute(fileSystem)),
+ Execute = arg => tabs.ToList().ForEach(tab => { // ToList -> because closing a tab modifies the original list
+ if (tab.Close.CanExecute(fileSystem)) {
+ tab.Close.Execute(fileSystem);
+ }
+ }),
+ };
+ undo = CreateWrapperFor(tab => tab.Undo);
+ redo = CreateWrapperFor(tab => tab.Redo);
}
public void Add(ITabContent content) {
@@ -103,20 +107,81 @@ namespace HavenSoft.Gen3Hex.ViewModel {
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
+ private StubCommand CreateWrapperFor(Func commandGetter) {
+ var command = new StubCommand {
+ CanExecute = arg => {
+ if (SelectedIndex < 0) return false;
+ var innerCommand = commandGetter(tabs[SelectedIndex]);
+ if (innerCommand == null) return false;
+ return innerCommand.CanExecute(fileSystem);
+ },
+ Execute = arg => {
+ var innerCommand = commandGetter(tabs[SelectedIndex]);
+ if (innerCommand == null) return;
+ innerCommand.Execute(fileSystem);
+ }
+ };
+
+ return command;
+ }
+
private void RemoveTab(object sender, EventArgs e) {
var tab = (ITabContent)sender;
if (!tabs.Contains(tab)) throw new InvalidOperationException("Cannot remove tab, because tab is not currently in editor.");
var index = tabs.IndexOf(tab);
- tabs.Remove(tab);
- tab.Closed -= RemoveTab;
- CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, tab));
- if (index < SelectedIndex) {
- selectedIndex--;
- } else if (index == SelectedIndex) {
- SelectedIndex = Math.Max(0, selectedIndex - 1);
+
+ if (index == SelectedIndex) {
+ StopListeningToCommandsFromCurrentTab();
+ tabs.Remove(tab);
+ tab.Closed -= RemoveTab;
+ if (selectedIndex == tabs.Count) TryUpdate(ref selectedIndex, tabs.Count - 1, nameof(SelectedIndex));
+ CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, tab, index));
+ StartListeningToCommandsFromCurrentTab();
+ return;
}
- if (tabs.Count == 0) SelectedIndex = -1;
+ if (index < SelectedIndex) selectedIndex--;
+
+ tabs.Remove(tab);
+ tab.Closed -= RemoveTab;
+ CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, tab, index));
}
+
+ private void StartListeningToCommandsFromCurrentTab() {
+ var commandsToRefresh = new List {
+ undo,
+ redo,
+ save,
+ saveAs,
+ close,
+ };
+ commandsToRefresh.ForEach(command => command.CanExecuteChanged.Invoke(command, EventArgs.Empty));
+
+ if (selectedIndex == -1) return;
+ var tab = tabs[selectedIndex];
+
+ if (tab.Save != null) tab.Save.CanExecuteChanged += RaiseSaveCanExecutedChanged;
+ if (tab.SaveAs != null) tab.SaveAs.CanExecuteChanged += RaiseSaveAsCanExecutedChanged;
+ if (tab.Close != null) tab.Close.CanExecuteChanged += RaiseCloseCanExecutedChanged;
+ if (tab.Undo != null) tab.Undo.CanExecuteChanged += RaiseUndoCanExecutedChanged;
+ if (tab.Redo != null) tab.Redo.CanExecuteChanged += RaiseRedoCanExecutedChanged;
+ }
+
+ private void StopListeningToCommandsFromCurrentTab() {
+ if (selectedIndex == -1) return;
+ var tab = tabs[selectedIndex];
+
+ if (tab.Save != null) tab.Save.CanExecuteChanged -= RaiseSaveCanExecutedChanged;
+ if (tab.SaveAs != null) tab.SaveAs.CanExecuteChanged -= RaiseSaveAsCanExecutedChanged;
+ if (tab.Close != null) tab.Close.CanExecuteChanged -= RaiseCloseCanExecutedChanged;
+ if (tab.Undo != null) tab.Undo.CanExecuteChanged -= RaiseUndoCanExecutedChanged;
+ if (tab.Redo != null) tab.Redo.CanExecuteChanged -= RaiseRedoCanExecutedChanged;
+ }
+
+ private void RaiseSaveCanExecutedChanged(object sender, EventArgs e) => save.CanExecuteChanged.Invoke(this, e);
+ private void RaiseSaveAsCanExecutedChanged(object sender, EventArgs e) => saveAs.CanExecuteChanged.Invoke(this, e);
+ private void RaiseCloseCanExecutedChanged(object sender, EventArgs e) => close.CanExecuteChanged.Invoke(this, e);
+ private void RaiseUndoCanExecutedChanged(object sender, EventArgs e) => undo.CanExecuteChanged.Invoke(this, e);
+ private void RaiseRedoCanExecutedChanged(object sender, EventArgs e) => redo.CanExecuteChanged.Invoke(this, e);
}
}
diff --git a/Gen3Hex/ViewModel/ScrollRegion.cs b/Gen3Hex/ViewModel/ScrollRegion.cs
index eacc1991..397292a1 100644
--- a/Gen3Hex/ViewModel/ScrollRegion.cs
+++ b/Gen3Hex/ViewModel/ScrollRegion.cs
@@ -31,14 +31,14 @@ namespace HavenSoft.Gen3Hex.ViewModel {
public int Width {
get => width;
set {
- if (TryUpdate(ref width, value)) UpdateScrollRange();
+ if (TryUpdate(ref width, value.LimitToRange(4, int.MaxValue))) UpdateScrollRange();
}
}
public int Height {
get => height;
set {
- if (TryUpdate(ref height, value)) UpdateScrollRange();
+ if (TryUpdate(ref height, value.LimitToRange(4, int.MaxValue))) UpdateScrollRange();
}
}
@@ -71,6 +71,8 @@ namespace HavenSoft.Gen3Hex.ViewModel {
public event EventHandler ScrollChanged;
public ScrollRegion() {
+ width = 4;
+ height = 4;
scroll = new StubCommand {
CanExecute = args => dataLength > 0,
Execute = args => ScrollExecuted((Direction)args),
diff --git a/Gen3Hex/ViewModel/ViewPort.cs b/Gen3Hex/ViewModel/ViewPort.cs
index 6f2ff084..b3d9074b 100644
--- a/Gen3Hex/ViewModel/ViewPort.cs
+++ b/Gen3Hex/ViewModel/ViewPort.cs
@@ -165,6 +165,8 @@ namespace HavenSoft.Gen3Hex.ViewModel {
CanExecute = arg => true,
Execute = arg => CloseExecuted((IFileSystem)arg),
};
+
+ RefreshBackingData();
}
public bool IsSelected(Point point) => selection.IsSelected(point);
diff --git a/HexTests/GeneralAppTests.cs b/HexTests/GeneralAppTests.cs
index 102143a7..321497da 100644
--- a/HexTests/GeneralAppTests.cs
+++ b/HexTests/GeneralAppTests.cs
@@ -59,7 +59,7 @@ namespace HavenSoft.HexTests {
Assert.Equal(1, executeCounter);
editor.SelectedIndex = 0;
- Assert.Null(editor.Undo);
+ Assert.False(editor.Undo.CanExecute(null));
}
[Fact]
@@ -168,6 +168,75 @@ namespace HavenSoft.HexTests {
Assert.Equal(0, editor.Count);
}
+ [Fact]
+ public void SaveCommandsPassInFileSystemAsParameter() {
+ int count = 0;
+ void checkIfArgIsFileSystem(object arg) { if (arg is IFileSystem) count++; }
+ bool canExecuteWrapper(object arg) { checkIfArgIsFileSystem(arg); return true; }
+ var save = new StubCommand { CanExecute = canExecuteWrapper, Execute = checkIfArgIsFileSystem };
+
+ editor.Add(new StubTabContent { Save = save, SaveAs = save });
+ editor.Add(new StubTabContent { Save = save, SaveAs = save });
+ editor.Add(new StubTabContent { Save = save, SaveAs = save });
+
+ editor.Save.Execute(); // once
+ editor.SaveAs.Execute(); // once
+ editor.SaveAll.Execute(); // 6 times, since SaveAll should also check CanExecute
+
+ Assert.Equal(8, count);
+ }
+
+ [Fact]
+ public void CloseCommandsPassInFileSystemAsParameter() {
+ int count = 0;
+ void checkIfArgIsFileSystem(object arg) { if (arg is IFileSystem) count++; }
+ bool canExecuteWrapper(object arg) { checkIfArgIsFileSystem(arg); return true; }
+ var close = new StubCommand { CanExecute = canExecuteWrapper, Execute = checkIfArgIsFileSystem };
+
+ editor.Add(new StubTabContent { Close = close });
+ editor.Add(new StubTabContent { Close = close });
+ editor.Add(new StubTabContent { Close = close });
+
+ editor.Close.Execute(); // once
+ editor.CloseAll.Execute(); // 6 times, since SaveAll should also check CanExecute
+
+ Assert.Equal(7, count);
+ }
+
+ [Fact]
+ public void EditorNotifiesCanExecuteChangedOnTabChange() {
+ int count = 0;
+ editor.Save.CanExecuteChanged += (sender, e) => count++;
+ editor.SaveAs.CanExecuteChanged += (sender, e) => count++;
+ editor.Close.CanExecuteChanged += (sender, e) => count++;
+ editor.Undo.CanExecuteChanged += (sender, e) => count++;
+ editor.Redo.CanExecuteChanged += (sender, e) => count++;
+ var tab = new StubTabContent();
+ tab.Close = new StubCommand { CanExecute = arg => true, Execute = arg => tab.Closed.Invoke(tab, EventArgs.Empty) };
+
+ editor.Add(tab);
+ Assert.Equal(5, count);
+
+ count = 0;
+ editor.Close.Execute();
+ Assert.Equal(5, count);
+ }
+
+ [Fact]
+ public void EditorNotifiesWhenUndoCanExecuteChange() {
+ int count = 0;
+ var undo = new StubCommand();
+ var tab = new StubTabContent { Undo = undo };
+ editor.Add(tab);
+ editor.Undo.CanExecuteChanged += (sender, e) => count++;
+
+ undo.CanExecute = arg => true;
+ undo.CanExecuteChanged.Invoke(undo, EventArgs.Empty);
+
+ Assert.Equal(1, count);
+ }
+
+
private StubTabContent CreateClosableTab() {
var tab = new StubTabContent();
var close = new StubCommand { CanExecute = arg => true };
diff --git a/HexTests/ViewPortCursorTests.cs b/HexTests/ViewPortCursorTests.cs
index ce814fb7..ce61f112 100644
--- a/HexTests/ViewPortCursorTests.cs
+++ b/HexTests/ViewPortCursorTests.cs
@@ -43,8 +43,8 @@ namespace HavenSoft.HexTests {
[Fact]
public void MovingCursorDownFromBottomRowScrolls() {
var loadedFile = new LoadedFile("test", new byte[25]);
- var viewPort = new ViewPort(loadedFile) { Width = 5, Height = 3 };
- viewPort.SelectionStart = new Point(0, 2);
+ var viewPort = new ViewPort(loadedFile) { Width = 5, Height = 4 };
+ viewPort.SelectionStart = new Point(0, 3);
viewPort.MoveSelectionStart.Execute(Direction.Down);
diff --git a/HexTests/ViewPortScrollTests.cs b/HexTests/ViewPortScrollTests.cs
index 10655843..34f301d4 100644
--- a/HexTests/ViewPortScrollTests.cs
+++ b/HexTests/ViewPortScrollTests.cs
@@ -96,8 +96,8 @@ namespace HavenSoft.HexTests {
[Fact]
public void ResizingCannotLeaveTotallyBlankLineAtTop() {
- var loadedFile = new LoadedFile("test", new byte[25]);
- var viewPort = new ViewPort(loadedFile) { Width = 5, Height = 5 };
+ var loadedFile = new LoadedFile("test", new byte[36]);
+ var viewPort = new ViewPort(loadedFile) { Width = 6, Height = 6 };
viewPort.ScrollValue++; // scroll down one line
viewPort.Width--; // decrease the width so that there is data 2 lines above