diff --git a/Gen3Hex/View/HexContent.cs b/Gen3Hex/View/HexContent.cs index ddb95639..f3302426 100644 --- a/Gen3Hex/View/HexContent.cs +++ b/Gen3Hex/View/HexContent.cs @@ -99,15 +99,22 @@ namespace HavenSoft.Gen3Hex.View { Focus(); ViewPort.SelectionStart = ControlCoordinatesToModelCoordinates(e); + CaptureMouse(); } protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); - if (e.LeftButton != MouseButtonState.Pressed) return; + if (!IsMouseCaptured) return; ViewPort.SelectionEnd = ControlCoordinatesToModelCoordinates(e); } + protected override void OnMouseUp(MouseButtonEventArgs e) { + base.OnMouseUp(e); + if (!IsMouseCaptured) return; + ReleaseMouseCapture(); + } + protected override void OnMouseWheel(MouseWheelEventArgs e) { base.OnMouseWheel(e); ViewPort.ScrollValue -= Math.Sign(e.Delta); diff --git a/Gen3Hex/View/MainWindow.xaml b/Gen3Hex/View/MainWindow.xaml index 0bd20b33..f18379a0 100644 --- a/Gen3Hex/View/MainWindow.xaml +++ b/Gen3Hex/View/MainWindow.xaml @@ -3,7 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:hsg3hv="clr-namespace:HavenSoft.Gen3Hex.View" xmlns:solarized="clr-namespace:Solarized;assembly=HavenSoft" - Title="Gen3Hex" Height="450" Width="800" Background="{solarized:Theme Background}"> + Title="Gen3Hex" Height="450" Width="800" AllowDrop="True" Background="{solarized:Theme Background}"> diff --git a/Gen3Hex/View/MainWindow.xaml.cs b/Gen3Hex/View/MainWindow.xaml.cs index 439d09ca..84ae50ca 100644 --- a/Gen3Hex/View/MainWindow.xaml.cs +++ b/Gen3Hex/View/MainWindow.xaml.cs @@ -1,6 +1,9 @@ -using HavenSoft.Gen3Hex.ViewModel; +using HavenSoft.Gen3Hex.Model; +using HavenSoft.Gen3Hex.ViewModel; using System; using System.ComponentModel; +using System.IO; +using System.Windows; namespace HavenSoft.Gen3Hex.View { public partial class MainWindow { @@ -12,6 +15,18 @@ namespace HavenSoft.Gen3Hex.View { DataContext = viewModel; } + protected override void OnDrop(DragEventArgs e) { + base.OnDrop(e); + + if (e.Data.GetDataPresent(DataFormats.FileDrop)) { + var files = (string[])e.Data.GetData(DataFormats.FileDrop); + foreach (var fileName in files) { + var data = File.ReadAllBytes(fileName); + ViewModel.Open.Execute(new LoadedFile(fileName, data)); + } + } + } + protected override void OnClosing(CancelEventArgs e) { base.OnClosing(e); ViewModel.CloseAll.Execute(); diff --git a/Gen3Hex/View/WindowsFileSystem.cs b/Gen3Hex/View/WindowsFileSystem.cs index 36aa771c..4458c579 100644 --- a/Gen3Hex/View/WindowsFileSystem.cs +++ b/Gen3Hex/View/WindowsFileSystem.cs @@ -23,7 +23,6 @@ namespace HavenSoft.Gen3Hex.View { } public bool Save(LoadedFile file) { - if (!File.Exists(file.Name)) return false; File.WriteAllBytes(file.Name, file.Contents); return true; } diff --git a/Gen3Hex/ViewModel/EditorViewModel.cs b/Gen3Hex/ViewModel/EditorViewModel.cs index 189f0cdb..6625903c 100644 --- a/Gen3Hex/ViewModel/EditorViewModel.cs +++ b/Gen3Hex/ViewModel/EditorViewModel.cs @@ -59,6 +59,7 @@ namespace HavenSoft.Gen3Hex.ViewModel { open = new StubCommand { CanExecute = arg => true, Execute = arg => { + var fileName = arg as string; var file = arg as LoadedFile; if (file == null) file = fileSystem.OpenFile(); if (file == null) return; diff --git a/Gen3Hex/ViewModel/Selection.cs b/Gen3Hex/ViewModel/Selection.cs index fb3fe97d..b654dc82 100644 --- a/Gen3Hex/ViewModel/Selection.cs +++ b/Gen3Hex/ViewModel/Selection.cs @@ -39,8 +39,8 @@ namespace HavenSoft.Gen3Hex.ViewModel { public Point SelectionEnd { get => selectionEnd; set { - if (scroll.Width == 0) scroll.Width = 0x10; - if (scroll.Height == 0) scroll.Height = 0x10; + var index = scroll.ViewPointToDataIndex(value); + value = scroll.DataIndexToViewPoint(index.LimitToRange(0, scroll.DataLength)); scroll.ScrollToPoint(ref value); TryUpdate(ref selectionEnd, value); diff --git a/Gen3Hex/ViewModel/ViewPort.cs b/Gen3Hex/ViewModel/ViewPort.cs index b3d9074b..3e0c8122 100644 --- a/Gen3Hex/ViewModel/ViewPort.cs +++ b/Gen3Hex/ViewModel/ViewPort.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Globalization; +using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Windows.Input; @@ -16,28 +17,23 @@ namespace HavenSoft.Gen3Hex.ViewModel { public class ViewPort : ViewModelCore, ITabContent, INotifyCollectionChanged { private static readonly NotifyCollectionChangedEventArgs ResetArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); - private readonly ScrollRegion scroll; - private readonly Selection selection; - private readonly ChangeHistory> history; - private readonly StubCommand save, saveAs, close; - private byte[] data; private HexElement[,] currentView; - - #region Name - - private string name; + private string fileName; public string Name { get { + var name = Path.GetFileNameWithoutExtension(fileName); + if (string.IsNullOrEmpty(name)) name = "Untitled"; + if (!history.IsSaved) name += "*"; return name; } } - #endregion - #region Scrolling Properties + private readonly ScrollRegion scroll; + public int Width { get => scroll.Width; set => selection.ChangeWidth(value); @@ -59,10 +55,24 @@ namespace HavenSoft.Gen3Hex.ViewModel { public ICommand Scroll => scroll.Scroll; + private void ScrollPropertyChanged(object sender, PropertyChangedEventArgs e) { + if (e.PropertyName == nameof(scroll.DataIndex)) { + RefreshBackingData(); + } else if (e.PropertyName != nameof(scroll.DataLength)) { + NotifyPropertyChanged(e.PropertyName); + } + + if (e.PropertyName == nameof(Width) || e.PropertyName == nameof(Height)) { + RefreshBackingData(); + } + } + #endregion #region Selection Properties + private readonly Selection selection; + public Point SelectionStart { get => selection.SelectionStart; set => selection.SelectionStart = value; @@ -87,10 +97,17 @@ namespace HavenSoft.Gen3Hex.ViewModel { } } + private void SelectionPropertyChanged(object sender, PropertyChangedEventArgs e) { + if (e.PropertyName == nameof(SelectionEnd)) history.ChangeCompleted(); + NotifyPropertyChanged(e.PropertyName); + } + #endregion #region Undo / Redo + private readonly ChangeHistory> history; + public ICommand Undo => history.Undo; public ICommand Redo => history.Redo; @@ -112,10 +129,18 @@ namespace HavenSoft.Gen3Hex.ViewModel { return opposite; } + private void HistoryPropertyChanged(object sender, PropertyChangedEventArgs e) { + if (e.PropertyName != nameof(history.IsSaved)) return; + save.CanExecuteChanged.Invoke(save, EventArgs.Empty); + NotifyPropertyChanged(nameof(Name)); + } + #endregion #region Saving + private readonly StubCommand save, saveAs, close; + public ICommand Save => save; public ICommand SaveAs => saveAs; @@ -124,6 +149,35 @@ namespace HavenSoft.Gen3Hex.ViewModel { public event EventHandler Closed; + private void SaveExecuted(IFileSystem fileSystem) { + if (history.IsSaved) return; + + if (string.IsNullOrEmpty(fileName)) { + SaveAsExecuted(fileSystem); + return; + } + + if (fileSystem.Save(new LoadedFile(fileName, data))) history.TagAsSaved(); + } + + private void SaveAsExecuted(IFileSystem fileSystem) { + var newName = fileSystem.RequestNewName(fileName); + if (newName == null) return; + + if (fileSystem.Save(new LoadedFile(newName, data))) { + fileName = newName; // don't bother notifying, because tagging the history will cause a notify; + history.TagAsSaved(); + } + } + + private void CloseExecuted(IFileSystem fileSystem) { + if (!history.IsSaved) { + var result = fileSystem.TrySavePrompt(new LoadedFile(fileName, data)); + if (result == null) return; + } + Closed?.Invoke(this, EventArgs.Empty); + } + #endregion public HexElement this[int x, int y] { @@ -140,7 +194,7 @@ namespace HavenSoft.Gen3Hex.ViewModel { public ViewPort() : this(new LoadedFile(string.Empty, new byte[0])) { } public ViewPort(LoadedFile file) { - name = file.Name; + fileName = file.Name; data = file.Contents; scroll = new ScrollRegion { DataLength = data.Length }; @@ -284,57 +338,6 @@ namespace HavenSoft.Gen3Hex.ViewModel { NotifyCollectionChanged(ResetArgs); } - private void ScrollPropertyChanged(object sender, PropertyChangedEventArgs e) { - if (e.PropertyName == nameof(scroll.DataIndex)) { - RefreshBackingData(); - } else if (e.PropertyName != nameof(scroll.DataLength)) { - NotifyPropertyChanged(e.PropertyName); - } - - if (e.PropertyName == nameof(Width) || e.PropertyName == nameof(Height)) { - RefreshBackingData(); - } - } - - private void SelectionPropertyChanged(object sender, PropertyChangedEventArgs e) { - if (e.PropertyName == nameof(SelectionEnd)) history.ChangeCompleted(); - NotifyPropertyChanged(e.PropertyName); - } - - private void HistoryPropertyChanged(object sender, PropertyChangedEventArgs e) { - if (e.PropertyName != nameof(history.IsSaved)) return; - save.CanExecuteChanged.Invoke(save, EventArgs.Empty); - } - - private void SaveExecuted(IFileSystem fileSystem) { - if (history.IsSaved) return; - - if (string.IsNullOrEmpty(name)) { - SaveAsExecuted(fileSystem); - return; - } - - if (fileSystem.Save(new LoadedFile(name, data))) history.TagAsSaved(); - } - - private void SaveAsExecuted(IFileSystem fileSystem) { - var newName = fileSystem.RequestNewName(name); - if (newName == null) return; - - if (fileSystem.Save(new LoadedFile(newName, data))) { - TryUpdate(ref name, newName, nameof(Name)); - history.TagAsSaved(); - } - } - - private void CloseExecuted(IFileSystem fileSystem) { - if (!history.IsSaved) { - var result = fileSystem.TrySavePrompt(new LoadedFile(name, data)); - if (result == null) return; - } - Closed?.Invoke(this, EventArgs.Empty); - } - private void NotifyCollectionChanged(NotifyCollectionChangedEventArgs args) => CollectionChanged?.Invoke(this, args); } } diff --git a/HexTests/ViewPortCursorTests.cs b/HexTests/ViewPortCursorTests.cs index ce61f112..d6611bea 100644 --- a/HexTests/ViewPortCursorTests.cs +++ b/HexTests/ViewPortCursorTests.cs @@ -200,5 +200,14 @@ namespace HavenSoft.HexTests { Assert.NotEqual(0, viewPort.Width); Assert.NotEqual(0, viewPort.Height); } + + [Fact] + public void CannotMoveSelectEndFarPassedEndOfFile() { + var selection = new Selection(new ScrollRegion { DataLength = 8 }); + + selection.SelectionEnd = new Point(3, 3); + + Assert.Equal(new Point(0, 2), selection.SelectionEnd); + } } } diff --git a/HexTests/ViewPortSaveTests.cs b/HexTests/ViewPortSaveTests.cs index 52158f9d..d0f36b2e 100644 --- a/HexTests/ViewPortSaveTests.cs +++ b/HexTests/ViewPortSaveTests.cs @@ -173,5 +173,56 @@ namespace HavenSoft.HexTests { Assert.Equal(2, canExecuteChangedCount); } + + [Fact] + public void ViewPortNameDoesNotContainPathOrExension() { + var viewPort = new ViewPort(new LoadedFile("path/to/myfile.txt", new byte[10])); + + Assert.Equal("myfile", viewPort.Name); + } + + [Fact] + public void ViewPortNameEndsWithStarIfNeedsSave() { + var viewPort = new ViewPort(new LoadedFile("path/to/myfile.txt", new byte[10])); + int nameChangedCount = 0; + viewPort.PropertyChanged += (sender, e) => { if (e.PropertyName == nameof(viewPort.Name)) nameChangedCount++; }; + + viewPort.Edit("12 34 56"); + + Assert.EndsWith("*", viewPort.Name); + Assert.Equal(1, nameChangedCount); + } + + [Fact] + public void ViewPortHasDefaultNameBeforeFirstSave() { + var viewPort = new ViewPort(); + + Assert.NotEqual(string.Empty, viewPort.Name); + Assert.NotNull(viewPort.Name); + } + + [Fact] + public void EditDefaultFileStillShowsStar() { + var viewPort = new ViewPort(); + var name = viewPort.Name; + + viewPort.Edit("11 22 33"); + + Assert.Equal($"{name}*", viewPort.Name); + } + + [Fact] + public void ViewPortTakesNewNameOnSave() { + var fileSystem = new StubFileSystem { RequestNewName = (originalName, extensions) => "path/to/newfile.txt", Save = loadedFile => true }; + var viewPort = new ViewPort(); + int nameChangedCount = 0; + viewPort.PropertyChanged += (sender, e) => { if (e.PropertyName == nameof(viewPort.Name)) nameChangedCount++; }; + + viewPort.Edit("012345"); + viewPort.Save.Execute(fileSystem); + + Assert.Equal("newfile", viewPort.Name); + Assert.Equal(2, nameChangedCount); + } } }