diff --git a/Gen3Hex/Model/IFileSystem.cs b/Gen3Hex/Model/IFileSystem.cs index 8811ac8d..4a7304f4 100644 --- a/Gen3Hex/Model/IFileSystem.cs +++ b/Gen3Hex/Model/IFileSystem.cs @@ -1,4 +1,6 @@ -namespace HavenSoft.Gen3Hex.Model { +using System; + +namespace HavenSoft.Gen3Hex.Model { public interface IFileSystem { string CopyText { get; set; } @@ -34,6 +36,22 @@ /// LoadedFile OpenFile(params string[] extensionOptions); + /// + /// Have the filesystem open a specific file. + /// + /// + /// If the file exists, it is loaded and returned. + /// If it doesn't exist, returns null. + /// + LoadedFile LoadFile(string fileName); + + /// + /// When a file changes, the filesystem will call all listeners for that file. + /// + void AddListenerToFile(string fileName, Action listener); + + void RemoveAllListenersForFile(string fileName); + /// /// Saves the file without prompting the user for permission. /// diff --git a/Gen3Hex/View/WindowsFileSystem.cs b/Gen3Hex/View/WindowsFileSystem.cs index bfa9e425..61b5d6cc 100644 --- a/Gen3Hex/View/WindowsFileSystem.cs +++ b/Gen3Hex/View/WindowsFileSystem.cs @@ -1,11 +1,15 @@ using HavenSoft.Gen3Hex.Model; using Microsoft.Win32; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Windows; namespace HavenSoft.Gen3Hex.View { public class WindowsFileSystem : IFileSystem { + private readonly Dictionary> watchers = new Dictionary>(); + public string CopyText { get => Clipboard.ContainsText() ? Clipboard.GetText() : string.Empty; set => Clipboard.SetText(value); @@ -15,9 +19,31 @@ namespace HavenSoft.Gen3Hex.View { 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); + return LoadFile(dialog.FileName); + } + + public LoadedFile LoadFile(string fileName) { + if (!File.Exists(fileName)) return null; + var data = File.ReadAllBytes(fileName); + return new LoadedFile(fileName, data); + } + + public void AddListenerToFile(string fileName, Action listener) { + var watcher = new FileSystemWatcher(Path.GetDirectoryName(fileName)) { + NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.LastWrite | NotifyFilters.FileName, + }; + watcher.Changed += (sender, e) => { + if (e.FullPath.EndsWith(fileName)) listener(this); + }; + watcher.EnableRaisingEvents = true; + + if (!watchers.ContainsKey(fileName)) watchers[fileName] = new List(); + watchers[fileName].Add(watcher); + } + + public void RemoveAllListenersForFile(string fileName) { + if (!watchers.ContainsKey(fileName)) return; + watchers[fileName].Clear(); } public string RequestNewName(string currentName, params string[] extensionOptions) { diff --git a/Gen3Hex/ViewModel/ITabContent.cs b/Gen3Hex/ViewModel/ITabContent.cs index 91b1642c..acc23012 100644 --- a/Gen3Hex/ViewModel/ITabContent.cs +++ b/Gen3Hex/ViewModel/ITabContent.cs @@ -1,4 +1,5 @@ -using System; +using HavenSoft.Gen3Hex.Model; +using System; using System.Windows.Input; namespace HavenSoft.Gen3Hex.ViewModel { diff --git a/Gen3Hex/ViewModel/IViewPort.cs b/Gen3Hex/ViewModel/IViewPort.cs index ff69fe3e..dc40dd4c 100644 --- a/Gen3Hex/ViewModel/IViewPort.cs +++ b/Gen3Hex/ViewModel/IViewPort.cs @@ -22,5 +22,6 @@ namespace HavenSoft.Gen3Hex.ViewModel { IReadOnlyList Find(string search); IChildViewPort CreateChildView(int offset); void FollowLink(int x, int y); + void ConsiderReload(IFileSystem fileSystem); } } diff --git a/Gen3Hex/ViewModel/SearchResultsViewPort.cs b/Gen3Hex/ViewModel/SearchResultsViewPort.cs index e7772246..ce7c01b1 100644 --- a/Gen3Hex/ViewModel/SearchResultsViewPort.cs +++ b/Gen3Hex/ViewModel/SearchResultsViewPort.cs @@ -116,6 +116,8 @@ namespace HavenSoft.Gen3Hex.ViewModel { RequestTabChange?.Invoke(this, parent); } + public void ConsiderReload(IFileSystem fileSystem) { } + private void NotifyCollectionChanged() { if (children.Count == 0) return; UpdateHeaders(); diff --git a/Gen3Hex/ViewModel/ViewPort.cs b/Gen3Hex/ViewModel/ViewPort.cs index 993a4f85..ca511187 100644 --- a/Gen3Hex/ViewModel/ViewPort.cs +++ b/Gen3Hex/ViewModel/ViewPort.cs @@ -301,6 +301,10 @@ namespace HavenSoft.Gen3Hex.ViewModel { public void FollowLink(int x, int y) { } + public void ConsiderReload(IFileSystem fileSystem) { + + } + private void Edit(char input) { var point = GetEditPoint(); var element = currentView[point.X, point.Y]; diff --git a/HexTests/GeneralAppTests.cs b/HexTests/GeneralAppTests.cs index 2e456298..9a10dfcd 100644 --- a/HexTests/GeneralAppTests.cs +++ b/HexTests/GeneralAppTests.cs @@ -204,7 +204,6 @@ namespace HavenSoft.HexTests { Assert.Equal(7, count); } - [Theory] [InlineData(nameof(EditorViewModel.Copy))] [InlineData(nameof(EditorViewModel.Delete))] @@ -323,6 +322,58 @@ namespace HavenSoft.HexTests { Assert.Equal(1, editor.SelectedIndex); } + [Fact] + public void EditorAddsOpenedFilesToFileSystemWatch() { + var fileSystem = new StubFileSystem(); + string name = null; + fileSystem.AddListenerToFile = (fileName, action) => name = fileName; + var editor = new EditorViewModel(fileSystem); + + editor.Open.Execute(new LoadedFile("InputFile.txt", new byte[20])); + + Assert.Equal("InputFile.txt", name); + } + + [Fact] + public void EditorRemovesFileSystemWatchWhenTabsClose() { + var fileSystem = new StubFileSystem(); + string name = null; + fileSystem.RemoveAllListenersForFile = fileName => name = fileName; + var editor = new EditorViewModel(fileSystem); + editor.Open.Execute(new LoadedFile("InputFile.txt", new byte[20])); + + editor.Close.Execute(); + + Assert.Equal("InputFile.txt", name); + } + + [Fact] + public void ViewPortReloadsIfNoLocalChangesWhenFileChanges() { + var fileSystem = new StubFileSystem(); + string file = null; + fileSystem.LoadFile = input => { file = input; return new LoadedFile(input, new byte[] { 0x10, 0x20 }); }; + var viewPort = new ViewPort(new LoadedFile("file.txt", new byte[] { 0x00, 0x00 })); + + viewPort.ConsiderReload(fileSystem); + + Assert.Equal("file.txt", file); + Assert.Equal(0x10, viewPort[0, 0].Value); + } + + [Fact] + public void ViewPortDoesNotReloadIfLocalChangesWhenFileChanges() { + var fileSystem = new StubFileSystem(); + string file = null; + fileSystem.LoadFile = input => { file = input; return new LoadedFile(input, new byte[] { 0x10, 0x20 }); }; + var viewPort = new ViewPort(new LoadedFile("file.txt", new byte[] { 0x00, 0x00 })); + + viewPort.Edit("05"); + viewPort.ConsiderReload(fileSystem); + + Assert.Null(file); + Assert.Equal(0x05, viewPort[0, 0].Value); + } + private StubTabContent CreateClosableTab() { var tab = new StubTabContent(); var close = new StubCommand { CanExecute = arg => true };