From e4671311f46fcd048009b8b4e7139c933ecd4ecb Mon Sep 17 00:00:00 2001 From: diluculo Date: Thu, 25 Aug 2016 13:39:26 +0900 Subject: [PATCH] Add RecentFiles module --- src/Gemini/Framework/Document.cs | 9 +- src/Gemini/Framework/IPersistedDocument.cs | 1 + src/Gemini/Framework/Services/IShell.cs | 3 + src/Gemini/Gemini.csproj | 8 + .../Modules/MainMenu/MenuDefinitions.cs | 3 + .../Commands/OpenRecentFileCommandHandler.cs | 64 ++++++++ .../OpenRecentFileCommandListDefinition.cs | 15 ++ .../Commands/RecentFilesCommandDefinition.cs | 26 ++++ .../Commands/RecentFilesCommandHandler.cs | 29 ++++ .../Modules/RecentFiles/IRecentFiles.cs | 12 ++ .../Modules/RecentFiles/MenuDefinitions.cs | 26 ++++ .../ViewModels/RecentFileItemViewModel.cs | 71 +++++++++ .../ViewModels/RecentFileViewModel.cs | 145 ++++++++++++++++++ .../Shell/Commands/OpenFileCommandHandler.cs | 13 +- .../Shell/ViewModels/ShellViewModel.cs | 29 +++- src/Gemini/Properties/Resources.Designer.cs | 4 +- src/Gemini/Properties/Settings.Designer.cs | 13 +- src/Gemini/Properties/Settings.settings | 3 + 18 files changed, 466 insertions(+), 8 deletions(-) create mode 100644 src/Gemini/Modules/RecentFiles/Commands/OpenRecentFileCommandHandler.cs create mode 100644 src/Gemini/Modules/RecentFiles/Commands/OpenRecentFileCommandListDefinition.cs create mode 100644 src/Gemini/Modules/RecentFiles/Commands/RecentFilesCommandDefinition.cs create mode 100644 src/Gemini/Modules/RecentFiles/Commands/RecentFilesCommandHandler.cs create mode 100644 src/Gemini/Modules/RecentFiles/IRecentFiles.cs create mode 100644 src/Gemini/Modules/RecentFiles/MenuDefinitions.cs create mode 100644 src/Gemini/Modules/RecentFiles/ViewModels/RecentFileItemViewModel.cs create mode 100644 src/Gemini/Modules/RecentFiles/ViewModels/RecentFileViewModel.cs diff --git a/src/Gemini/Framework/Document.cs b/src/Gemini/Framework/Document.cs index 6e54a651..143c178b 100644 --- a/src/Gemini/Framework/Document.cs +++ b/src/Gemini/Framework/Document.cs @@ -89,7 +89,8 @@ Task ICommandHandler.Run(Command command) void ICommandHandler.Update(Command command) { - command.Enabled = this is IPersistedDocument; + var persistedDocument = this as IPersistedDocument; + command.Enabled = (persistedDocument != null && persistedDocument.IsDirty); } async Task ICommandHandler.Run(Command command) @@ -138,7 +139,7 @@ private static async Task DoSaveAs(IPersistedDocument persistedDocument) if (fileType != null) filter = fileType.Name + "|*" + fileType.FileExtension + "|"; - filter += "All Files|*.*"; + filter += Properties.Resources.AllFiles + "|*.*"; dialog.Filter = filter; if (dialog.ShowDialog() != true) @@ -148,6 +149,10 @@ private static async Task DoSaveAs(IPersistedDocument persistedDocument) // Save file. await persistedDocument.Save(filePath); + + // Add to recent files + IShell _shell = IoC.Get(); + _shell.RecentFiles.Update(filePath); } } } \ No newline at end of file diff --git a/src/Gemini/Framework/IPersistedDocument.cs b/src/Gemini/Framework/IPersistedDocument.cs index 2455c8d9..1b30dc6f 100644 --- a/src/Gemini/Framework/IPersistedDocument.cs +++ b/src/Gemini/Framework/IPersistedDocument.cs @@ -5,6 +5,7 @@ namespace Gemini.Framework public interface IPersistedDocument : IDocument { bool IsNew { get; } + bool IsDirty { get; } string FileName { get; } string FilePath { get; } diff --git a/src/Gemini/Framework/Services/IShell.cs b/src/Gemini/Framework/Services/IShell.cs index 2765e04f..50943ae5 100644 --- a/src/Gemini/Framework/Services/IShell.cs +++ b/src/Gemini/Framework/Services/IShell.cs @@ -3,6 +3,7 @@ using Gemini.Modules.MainMenu; using Gemini.Modules.StatusBar; using Gemini.Modules.ToolBars; +using Gemini.Modules.RecentFiles; namespace Gemini.Framework.Services { @@ -16,6 +17,7 @@ public interface IShell : IGuardClose, IDeactivate IMenu MainMenu { get; } IToolBars ToolBars { get; } IStatusBar StatusBar { get; } + IRecentFiles RecentFiles { get; } // TODO: Rename this to ActiveItem. ILayoutItem ActiveLayoutItem { get; set; } @@ -29,6 +31,7 @@ public interface IShell : IGuardClose, IDeactivate void ShowTool() where TTool : ITool; void ShowTool(ITool model); + bool TryActivateDocumentByPath(string path); void OpenDocument(IDocument model); void CloseDocument(IDocument document); diff --git a/src/Gemini/Gemini.csproj b/src/Gemini/Gemini.csproj index f6148ae6..283e6595 100644 --- a/src/Gemini/Gemini.csproj +++ b/src/Gemini/Gemini.csproj @@ -178,6 +178,14 @@ MainMenuSettingsView.xaml + + + + + + + + diff --git a/src/Gemini/Modules/MainMenu/MenuDefinitions.cs b/src/Gemini/Modules/MainMenu/MenuDefinitions.cs index d2da8594..75c777ba 100644 --- a/src/Gemini/Modules/MainMenu/MenuDefinitions.cs +++ b/src/Gemini/Modules/MainMenu/MenuDefinitions.cs @@ -21,6 +21,9 @@ public static class MenuDefinitions [Export] public static MenuItemGroupDefinition FileSaveMenuGroup = new MenuItemGroupDefinition(FileMenu, 6); + [Export] + public static MenuItemGroupDefinition FileOpenRecentMenuGroup = new MenuItemGroupDefinition(FileMenu, 9); + [Export] public static MenuItemGroupDefinition FileExitOpenMenuGroup = new MenuItemGroupDefinition(FileMenu, 10); diff --git a/src/Gemini/Modules/RecentFiles/Commands/OpenRecentFileCommandHandler.cs b/src/Gemini/Modules/RecentFiles/Commands/OpenRecentFileCommandHandler.cs new file mode 100644 index 00000000..5a73450e --- /dev/null +++ b/src/Gemini/Modules/RecentFiles/Commands/OpenRecentFileCommandHandler.cs @@ -0,0 +1,64 @@ +using Caliburn.Micro; +using Gemini.Framework; +using Gemini.Framework.Commands; +using Gemini.Framework.Services; +using Gemini.Modules.Shell.Commands; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; + +namespace Gemini.Modules.RecentFiles.Commands +{ + [CommandHandler] + public class OpenRecentFileCommandHandler : ICommandListHandler + { + private readonly IShell _shell; + + [ImportingConstructor] + public OpenRecentFileCommandHandler(IShell shell) + { + _shell = shell; + } + + public void Populate(Command command, List commands) + { + for (var i = 0; i < _shell.RecentFiles.Items.Count; i++) + { + var item = _shell.RecentFiles.Items[i]; + commands.Add(new Command(command.CommandDefinition) + { + Text = string.Format("_{0} {1}", i + 1, item.DisplayName), + ToolTip = item.FilePath, + Tag = item.FilePath + }); + } + } + + public async Task Run(Command command) + { + var newPath = (string)command.Tag; + + // Check if the document is already open + foreach (var document in _shell.Documents.OfType().Where(d => !d.IsNew)) + { + if (string.IsNullOrEmpty(document.FilePath)) + continue; + + var docPath = Path.GetFullPath(document.FilePath); + if (string.Equals(newPath, docPath, System.StringComparison.OrdinalIgnoreCase)) + { + _shell.OpenDocument(document); + return; + } + } + + _shell.OpenDocument(await OpenFileCommandHandler.GetEditor(newPath)); + + // Add the file to the recent documents list + _shell.RecentFiles.Update(newPath); + } + } +} \ No newline at end of file diff --git a/src/Gemini/Modules/RecentFiles/Commands/OpenRecentFileCommandListDefinition.cs b/src/Gemini/Modules/RecentFiles/Commands/OpenRecentFileCommandListDefinition.cs new file mode 100644 index 00000000..d6802430 --- /dev/null +++ b/src/Gemini/Modules/RecentFiles/Commands/OpenRecentFileCommandListDefinition.cs @@ -0,0 +1,15 @@ +using Gemini.Framework.Commands; + +namespace Gemini.Modules.RecentFiles.Commands +{ + [CommandDefinition] + public class OpenRecentFileCommandListDefinition : CommandListDefinition + { + public const string CommandName = "File.OpenRecentFileList"; + + public override string Name + { + get { return CommandName; } + } + } +} \ No newline at end of file diff --git a/src/Gemini/Modules/RecentFiles/Commands/RecentFilesCommandDefinition.cs b/src/Gemini/Modules/RecentFiles/Commands/RecentFilesCommandDefinition.cs new file mode 100644 index 00000000..48e5491e --- /dev/null +++ b/src/Gemini/Modules/RecentFiles/Commands/RecentFilesCommandDefinition.cs @@ -0,0 +1,26 @@ +using Gemini.Framework.Commands; +using Gemini.Properties; + +namespace Gemini.Modules.RecentFiles.Commands +{ + [CommandDefinition] + public class RecentFilesCommandDefinition : CommandDefinition + { + public const string CommandName = "File.RecentFiles"; + + public override string Name + { + get { return CommandName; } + } + + public override string Text + { + get { return Resources.FileRecentFilesCommandText; } + } + + public override string ToolTip + { + get { return Resources.FileRecentFilesCommandToolTip; } + } + } +} \ No newline at end of file diff --git a/src/Gemini/Modules/RecentFiles/Commands/RecentFilesCommandHandler.cs b/src/Gemini/Modules/RecentFiles/Commands/RecentFilesCommandHandler.cs new file mode 100644 index 00000000..2e7d4f74 --- /dev/null +++ b/src/Gemini/Modules/RecentFiles/Commands/RecentFilesCommandHandler.cs @@ -0,0 +1,29 @@ +using Gemini.Framework.Commands; +using Gemini.Framework.Services; +using System.ComponentModel.Composition; +using System.Threading.Tasks; + +namespace Gemini.Modules.RecentFiles.Commands +{ + [CommandHandler] + public class RecentFilesCommandHandler : CommandHandlerBase + { + private readonly IShell _shell; + + [ImportingConstructor] + public RecentFilesCommandHandler(IShell shell) + { + _shell = shell; + } + + public override void Update(Command command) + { + command.Enabled = (_shell.RecentFiles.Items.Count > 0); + } + + public override Task Run(Command command) + { + return null; + } + } +} \ No newline at end of file diff --git a/src/Gemini/Modules/RecentFiles/IRecentFiles.cs b/src/Gemini/Modules/RecentFiles/IRecentFiles.cs new file mode 100644 index 00000000..0987d2c4 --- /dev/null +++ b/src/Gemini/Modules/RecentFiles/IRecentFiles.cs @@ -0,0 +1,12 @@ +using Caliburn.Micro; +using Gemini.Modules.RecentFiles.ViewModels; + +namespace Gemini.Modules.RecentFiles +{ + public interface IRecentFiles + { + IObservableCollection Items { get; } + + void Update(string filePath); + } +} \ No newline at end of file diff --git a/src/Gemini/Modules/RecentFiles/MenuDefinitions.cs b/src/Gemini/Modules/RecentFiles/MenuDefinitions.cs new file mode 100644 index 00000000..24879224 --- /dev/null +++ b/src/Gemini/Modules/RecentFiles/MenuDefinitions.cs @@ -0,0 +1,26 @@ +using Gemini.Framework.Menus; +using Gemini.Modules.RecentFiles.Commands; +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Gemini.Modules.RecentFiles +{ + public static class MenuDefinitions + { + [Export] + public static MenuItemDefinition FileRecentFilesMenuItem = new CommandMenuItemDefinition( + MainMenu.MenuDefinitions.FileOpenRecentMenuGroup, 0); + + [Export] + public static MenuItemGroupDefinition FileRecentFilesCascadeGroup = new MenuItemGroupDefinition( + FileRecentFilesMenuItem, 0); + + [Export] + public static MenuItemDefinition FileOpenRecentMenuItemList = new CommandMenuItemDefinition( + FileRecentFilesCascadeGroup, 0); + } +} \ No newline at end of file diff --git a/src/Gemini/Modules/RecentFiles/ViewModels/RecentFileItemViewModel.cs b/src/Gemini/Modules/RecentFiles/ViewModels/RecentFileItemViewModel.cs new file mode 100644 index 00000000..dd8dadb5 --- /dev/null +++ b/src/Gemini/Modules/RecentFiles/ViewModels/RecentFileItemViewModel.cs @@ -0,0 +1,71 @@ +using Caliburn.Micro; + +namespace Gemini.Modules.RecentFiles.ViewModels +{ + public class RecentFileItemViewModel : PropertyChangedBase + { + private int _index; + public int Index + { + get { return _index; } + internal set + { + _index = value; + NotifyOfPropertyChange(() => Index); + } + } + + private string _filePath; + public string FilePath + { + get { return _filePath; } + set + { + _filePath = value; + _displayName = ShortenPath(_filePath); + NotifyOfPropertyChange(() => FilePath); + } + } + + private string _displayName; + public string DisplayName + { + get { return _displayName; } + } + + // TODO: will implement Pinned + private bool _pinned = false; + public bool Pinned + { + get { return _pinned; } + set + { + _pinned = value; + NotifyOfPropertyChange(() => Pinned); + } + } + + public RecentFileItemViewModel(string filePath, bool pinned = false) + { + _filePath = filePath; + _displayName = ShortenPath(filePath); + + _pinned = pinned; + } + + // http://stackoverflow.com/questions/8360360/function-to-shrink-file-path-to-be-more-human-readable + private string ShortenPath(string path, int maxLength = 50) + { + string[] splits = path.Split('\\'); + + string output = ""; + + if (splits.Length > 4) + output = splits[0] + "\\" + splits[1] + "\\...\\" + splits[splits.Length - 2] + "\\" + splits[splits.Length - 1]; + else + output = string.Join("\\", splits, 0, splits.Length); + + return output; + } + } +} \ No newline at end of file diff --git a/src/Gemini/Modules/RecentFiles/ViewModels/RecentFileViewModel.cs b/src/Gemini/Modules/RecentFiles/ViewModels/RecentFileViewModel.cs new file mode 100644 index 00000000..0031f0de --- /dev/null +++ b/src/Gemini/Modules/RecentFiles/ViewModels/RecentFileViewModel.cs @@ -0,0 +1,145 @@ +using Caliburn.Micro; +using System.Collections.Specialized; +using System.ComponentModel.Composition; + +namespace Gemini.Modules.RecentFiles.ViewModels +{ + [Export(typeof(IRecentFiles))] + public class RecentFileViewModel : PropertyChangedBase, IRecentFiles + { + private readonly RecentFileItemCollection _items; + public IObservableCollection Items + { + get { return _items; } + } + + private int _maxItems = 10; + public int MaxItems + { + get { return _maxItems; } + set + { + _maxItems = value; + _items.MaxCollectionSize = _maxItems; + } + } + + public RecentFileViewModel() + { + _items = new RecentFileItemCollection(); + InitializeList(); + } + + /// + /// Adds or moves the file to the top of the list. + /// + /// + public void Update(string filePath) + { + RecentFileItemViewModel item = new RecentFileItemViewModel(filePath); + + int i = IndexOf(item); + if (i >= 0) + { + if (_items[i].Pinned) return; // do not move pinned items + RemoveAt(i); + } + + Insert(0, item); + SaveList(); + } + + private int IndexOf(string filePath) + { + for (int i = 0; i < _items.Count; ++i) + { + if (string.Equals(_items[i].FilePath, filePath, System.StringComparison.OrdinalIgnoreCase)) return i; + } + return -1; + } + + private int IndexOf(RecentFileItemViewModel item) + { + return IndexOf(item.FilePath); + } + + public void RemoveItem(string filePath) + { + RecentFileItemViewModel item = new RecentFileItemViewModel(filePath); + + int i = IndexOf(item); + if (i >= 0) + { + _items.RemoveAt(i); + SaveList(); + } + } + + private void RemoveAt(int i) + { + _items.RemoveAt(i); + } + + private void Insert(int i, RecentFileItemViewModel item) + { + _items.Insert(i, item); + } + + private void InitializeList() + { + // Create a new collection if it was not serialized before. + if (Properties.Settings.Default.RecentDocuments == null) + { + Properties.Settings.Default.RecentDocuments = new StringCollection(); + } + + foreach (string filePath in Properties.Settings.Default.RecentDocuments) + { + _items.Add(new RecentFileItemViewModel(filePath)); + } + } + + private void SaveList() + { + StringCollection docs = Properties.Settings.Default.RecentDocuments = new StringCollection(); + + foreach (var item in _items) + { + docs.Add(item.FilePath); + } + + Properties.Settings.Default.Save(); + } + + private class RecentFileItemCollection : BindableCollection + { + public int MaxCollectionSize { get; set; } + + public RecentFileItemCollection(int maxCollectionSize = 10) + : base() + { + MaxCollectionSize = maxCollectionSize; + } + + protected override void InsertItemBase(int index, RecentFileItemViewModel item) + { + item.Index = index; + base.InsertItemBase(index, item); + + if (MaxCollectionSize > 0 && MaxCollectionSize < Count) + { + for (int i = Count - 1; i >= MaxCollectionSize; i--) + { + RemoveAt(i); + } + } + } + + protected override void SetItemBase(int index, RecentFileItemViewModel item) + { + item.Index = index; + base.SetItemBase(index, item); + } + } + } +} \ No newline at end of file diff --git a/src/Gemini/Modules/Shell/Commands/OpenFileCommandHandler.cs b/src/Gemini/Modules/Shell/Commands/OpenFileCommandHandler.cs index 3e35da78..90213197 100644 --- a/src/Gemini/Modules/Shell/Commands/OpenFileCommandHandler.cs +++ b/src/Gemini/Modules/Shell/Commands/OpenFileCommandHandler.cs @@ -7,6 +7,7 @@ using Gemini.Framework.Commands; using Gemini.Framework.Services; using Microsoft.Win32; +using System.IO; namespace Gemini.Modules.Shell.Commands { @@ -27,7 +28,7 @@ public override async Task Run(Command command) { var dialog = new OpenFileDialog(); - dialog.Filter = "All Supported Files|" + string.Join(";", _editorProviders + dialog.Filter = Properties.Resources.AllSupportedFiles + "|" + string.Join(";", _editorProviders .SelectMany(x => x.FileTypes).Select(x => "*" + x.FileExtension)); dialog.Filter += "|" + string.Join("|", _editorProviders @@ -35,7 +36,15 @@ public override async Task Run(Command command) .Select(x => x.Name + "|*" + x.FileExtension)); if (dialog.ShowDialog() == true) - _shell.OpenDocument(await GetEditor(dialog.FileName)); + { + var fullPath = Path.GetFullPath(dialog.FileName); + + if (!_shell.TryActivateDocumentByPath(fullPath)) + _shell.OpenDocument(await GetEditor(fullPath)); + + // Add the file to the recent documents list + _shell.RecentFiles.Update(fullPath); + } } internal static Task GetEditor(string path) diff --git a/src/Gemini/Modules/Shell/ViewModels/ShellViewModel.cs b/src/Gemini/Modules/Shell/ViewModels/ShellViewModel.cs index 61625b62..48a3da50 100644 --- a/src/Gemini/Modules/Shell/ViewModels/ShellViewModel.cs +++ b/src/Gemini/Modules/Shell/ViewModels/ShellViewModel.cs @@ -13,6 +13,7 @@ using Gemini.Modules.Shell.Views; using Gemini.Modules.StatusBar; using Gemini.Modules.ToolBars; +using Gemini.Modules.RecentFiles; namespace Gemini.Modules.Shell.ViewModels { @@ -38,6 +39,9 @@ public class ShellViewModel : Conductor.Collection.OneActive, IShell [Import] private IStatusBar _statusBar; + [Import] + private IRecentFiles _recentFiles; + [Import] private ILayoutItemStatePersister _layoutItemStatePersister; #pragma warning restore 649 @@ -60,6 +64,11 @@ public IStatusBar StatusBar get { return _statusBar; } } + public IRecentFiles RecentFiles + { + get { return _recentFiles; } + } + private ILayoutItem _activeLayoutItem; public ILayoutItem ActiveLayoutItem { @@ -175,10 +184,28 @@ public void ShowTool(ITool model) ActiveLayoutItem = model; } + public bool TryActivateDocumentByPath(string path) + { + foreach (var document in Documents.OfType().Where(d => !d.IsNew)) + { + if (string.IsNullOrEmpty(document.FilePath)) + continue; + + var docPath = Path.GetFullPath(document.FilePath); + if (string.Equals(path, docPath, StringComparison.OrdinalIgnoreCase)) + { + OpenDocument(document); + return true; + } + } + + return false; + } + public void OpenDocument(IDocument model) { ActivateItem(model); - } + } public void CloseDocument(IDocument document) { diff --git a/src/Gemini/Properties/Resources.Designer.cs b/src/Gemini/Properties/Resources.Designer.cs index 64b8a843..6d1b7b55 100644 --- a/src/Gemini/Properties/Resources.Designer.cs +++ b/src/Gemini/Properties/Resources.Designer.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // Этот код создан программой. // Исполняемая версия:4.0.30319.42000 @@ -61,7 +61,7 @@ internal Resources() { } /// - /// Ищет локализованную строку, похожую на Failed to set value: {0}. + /// Looks up a localized string similar to _Edit. /// internal static string AdvancedSliderCommitErrorFormat { get { diff --git a/src/Gemini/Properties/Settings.Designer.cs b/src/Gemini/Properties/Settings.Designer.cs index 2ae634e9..3cf53883 100644 --- a/src/Gemini/Properties/Settings.Designer.cs +++ b/src/Gemini/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace Gemini.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); @@ -58,5 +58,16 @@ public string LanguageCode { this["LanguageCode"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + public global::System.Collections.Specialized.StringCollection RecentDocuments { + get { + return ((global::System.Collections.Specialized.StringCollection)(this["RecentDocuments"])); + } + set { + this["RecentDocuments"] = value; + } + } } } diff --git a/src/Gemini/Properties/Settings.settings b/src/Gemini/Properties/Settings.settings index 0636e8c3..1a6939f4 100644 --- a/src/Gemini/Properties/Settings.settings +++ b/src/Gemini/Properties/Settings.settings @@ -11,5 +11,8 @@ + + + \ No newline at end of file