From 7ab42ec1dec459425167a8a666840bc1a90a28c3 Mon Sep 17 00:00:00 2001 From: Aleksandar Pesic Date: Tue, 26 Nov 2019 22:03:22 +0100 Subject: [PATCH] Menu item keyboard accelerators implemented (#589) * Added icon to "Recent >" menu item. * Refactored app menu implementation, and implemented keyboard accelerators. Still doesn't work though. * Few things fixed, but still doesn't work. * Clicking menu items works. * Fixing an issue with resources. * New menu binding implemented. Still doesn't work though. * Menu displayed properly, except for "Settings". * Menu works, but it doesn't refresh. * Works. But needs to be polished. * All known issues with the new app menu solved. * Significantly simplifying app menu binding. * Removing some obsolete classes. * Menu binding mechanism additionally optimized for performances. * Removing unused translations. * Further refactoring app menu binding, in order to make it more general. * New window mechanism simplified. Terminal-launching code refactored to be more readable. A bug with "SSH Info" fixed. * Merging the latest changed. --- .../IAcceleratorKeyValidator.cs | 12 + .../Implementation/DefaultValueProvider.cs | 3 +- .../MainViewModel.cs | 244 ++++++++++++++---- .../Menu/AppMenuViewModel.cs | 39 +++ .../Menu/ExpandableMenuItemViewModel.cs | 41 +++ .../Menu/MenuItemKeyBindingViewModel.cs | 147 +++++++++++ .../Menu/MenuItemViewModel.cs | 46 ++++ .../Menu/MenuItemViewModelBase.cs | 84 ++++++ .../ProfileCommandViewModel.cs | 19 -- FluentTerminal.App/App.xaml.cs | 1 + .../AppMenuViewModelToFlyoutMenuConverter.cs | 39 +++ .../Converters/IconConverter.cs | 49 ++++ ...ModelBaseToMenuFlayoutItemBaseConverter.cs | 131 ++++++++++ .../Dialogs/CreateKeyBindingDialog.xaml.cs | 15 +- FluentTerminal.App/FluentTerminal.App.csproj | 4 + .../Services/AcceleratorKeyValidator.cs | 13 + .../Services/CommandHistoryService.cs | 55 ++-- FluentTerminal.App/Strings/en/Resources.resw | 20 +- FluentTerminal.App/Views/MainPage.xaml | 43 +-- FluentTerminal.App/Views/MainPage.xaml.cs | 27 +- FluentTerminal.App/Views/MenuExtension.cs | 32 +-- FluentTerminal.Models/ExecutedCommand.cs | 4 +- 22 files changed, 904 insertions(+), 164 deletions(-) create mode 100644 FluentTerminal.App.Services/IAcceleratorKeyValidator.cs create mode 100644 FluentTerminal.App.ViewModels/Menu/AppMenuViewModel.cs create mode 100644 FluentTerminal.App.ViewModels/Menu/ExpandableMenuItemViewModel.cs create mode 100644 FluentTerminal.App.ViewModels/Menu/MenuItemKeyBindingViewModel.cs create mode 100644 FluentTerminal.App.ViewModels/Menu/MenuItemViewModel.cs create mode 100644 FluentTerminal.App.ViewModels/Menu/MenuItemViewModelBase.cs delete mode 100644 FluentTerminal.App.ViewModels/ProfileCommandViewModel.cs create mode 100644 FluentTerminal.App/Converters/AppMenuViewModelToFlyoutMenuConverter.cs create mode 100644 FluentTerminal.App/Converters/IconConverter.cs create mode 100644 FluentTerminal.App/Converters/MenuItemViewModelBaseToMenuFlayoutItemBaseConverter.cs create mode 100644 FluentTerminal.App/Services/AcceleratorKeyValidator.cs diff --git a/FluentTerminal.App.Services/IAcceleratorKeyValidator.cs b/FluentTerminal.App.Services/IAcceleratorKeyValidator.cs new file mode 100644 index 0000000..d6437f6 --- /dev/null +++ b/FluentTerminal.App.Services/IAcceleratorKeyValidator.cs @@ -0,0 +1,12 @@ +using FluentTerminal.Models.Enums; + +namespace FluentTerminal.App.Services +{ + // This validator is needed because accelerator key has to be member of VirtualKey enum. + public interface IAcceleratorKeyValidator + { + bool Valid(int key); + + bool Valid(ExtendedVirtualKey key); + } +} \ No newline at end of file diff --git a/FluentTerminal.App.Services/Implementation/DefaultValueProvider.cs b/FluentTerminal.App.Services/Implementation/DefaultValueProvider.cs index ee7470e..fae915d 100644 --- a/FluentTerminal.App.Services/Implementation/DefaultValueProvider.cs +++ b/FluentTerminal.App.Services/Implementation/DefaultValueProvider.cs @@ -241,7 +241,8 @@ namespace FluentTerminal.App.Services.Implementation Command = nameof(Command.ShowSettings), Ctrl = true, Shift = true, - Key = (int)ExtendedVirtualKey.Comma + // I've had to replace ExtendedVirtualKey.Comma because it isn't a member of VirtualKey enum, thus cannot be shown in menu. + Key = (int)ExtendedVirtualKey.S } }; diff --git a/FluentTerminal.App.ViewModels/MainViewModel.cs b/FluentTerminal.App.ViewModels/MainViewModel.cs index 81ade27..e502be8 100644 --- a/FluentTerminal.App.ViewModels/MainViewModel.cs +++ b/FluentTerminal.App.ViewModels/MainViewModel.cs @@ -1,29 +1,23 @@ -using FluentTerminal.App.Services; +using FluentTerminal.App.Services; using FluentTerminal.App.Services.EventArgs; using FluentTerminal.App.Services.Utilities; -using FluentTerminal.App.ViewModels.Infrastructure; using FluentTerminal.Models; using FluentTerminal.Models.Enums; using GalaSoft.MvvmLight; -using GalaSoft.MvvmLight.Command; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; using System.Threading.Tasks; +using FluentTerminal.App.ViewModels.Menu; using FluentTerminal.Models.Messages; +using GalaSoft.MvvmLight.Command; namespace FluentTerminal.App.ViewModels { public class MainViewModel : ViewModelBase { - #region Constants - - private const int RecentItemsMaxCount = 10; - - #endregion Constants - private readonly IClipboardService _clipboardService; private readonly IDialogService _dialogService; private readonly IDispatcherTimer _dispatcherTimer; @@ -31,16 +25,17 @@ namespace FluentTerminal.App.ViewModels private readonly ISettingsService _settingsService; private readonly ITrayProcessCommunicationService _trayProcessCommunicationService; private readonly ICommandHistoryService _commandHistoryService; + private readonly IAcceleratorKeyValidator _acceleratorKeyValidator; private ApplicationSettings _applicationSettings; private string _background; private double _backgroundOpacity; private TerminalViewModel _selectedTerminal; private TabsPosition _tabsPosition; private string _windowTitle; - private List _keyBindings; + private IDictionary> _keyBindings; public MainViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService, IKeyboardCommandService keyboardCommandService, - IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService, ICommandHistoryService commandHistoryService) + IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService, ICommandHistoryService commandHistoryService, IAcceleratorKeyValidator acceleratorKeyValidator) { MessengerInstance.Register(this, OnApplicationSettingsChanged); MessengerInstance.Register(this, OnCurrentThemeChanged); @@ -50,6 +45,7 @@ namespace FluentTerminal.App.ViewModels MessengerInstance.Register(this, OnSshProfileDeleted); MessengerInstance.Register(this, OnTerminalOptionsChanged); MessengerInstance.Register(this, OnCommandHistoryChanged); + MessengerInstance.Register(this, OnKeyBindingChanged); _settingsService = settingsService; @@ -60,6 +56,7 @@ namespace FluentTerminal.App.ViewModels _clipboardService = clipboardService; _keyboardCommandService = keyboardCommandService; _commandHistoryService = commandHistoryService; + _acceleratorKeyValidator = acceleratorKeyValidator; _keyboardCommandService.RegisterCommandHandler(nameof(Command.NewTab), async () => await AddDefaultProfileAsync(NewTerminalLocation.Tab)); _keyboardCommandService.RegisterCommandHandler(nameof(Command.NewWindow), async () => await AddDefaultProfileAsync(NewTerminalLocation.Window)); @@ -111,39 +108,17 @@ namespace FluentTerminal.App.ViewModels TabsPosition = _applicationSettings.TabsPosition; AddLocalShellCommand = new RelayCommand(async () => await AddDefaultProfileAsync(NewTerminalLocation.Tab)); - AddSshShellCommand = new RelayCommand(async () => await AddSshProfileAsync(NewTerminalLocation.Tab)); - AddQuickShellCommand = new RelayCommand(async () => await AddQuickLaunchProfileAsync(NewTerminalLocation.Tab)); - ShowAboutCommand = new AsyncCommand(ShowAbout); - ShowSettingsCommand = new RelayCommand(ShowSettings); ApplicationView.CloseRequested += OnCloseRequest; ApplicationView.Closed += OnClosed; Terminals.CollectionChanged += OnTerminalsCollectionChanged; LoadKeyBindings(); - MessengerInstance.Register(this, message => LoadKeyBindings()); + + InitializeAppMenu(); } - private IList _recentCommands; - - public IList RecentCommands - { - get => _recentCommands ?? (_recentCommands = _commandHistoryService - .GetHistoryRecentFirst(top: RecentItemsMaxCount).Select(c => - new ProfileCommandViewModel(c.ShellProfile, - new RelayCommand(() => AddTabAsync(c.ShellProfile)))).ToList()); - private set => Set(ref _recentCommands, value); - } - - private void OnCommandHistoryChanged(CommandHistoryChangedMessage message) - { - _recentCommands = null; - - ApplicationView.RunOnDispatcherThread(() => RaisePropertyChanged(nameof(RecentCommands)), false); - } - - private void LoadKeyBindings() => - _keyBindings = _settingsService.GetCommandKeyBindings().Values.SelectMany(v => v).ToList(); + private void LoadKeyBindings() => _keyBindings = _settingsService.GetCommandKeyBindings(); private void OnClosed(object sender, EventArgs e) { @@ -158,10 +133,6 @@ namespace FluentTerminal.App.ViewModels _applicationSettings = null; AddLocalShellCommand = null; - AddSshShellCommand = null; - AddQuickShellCommand = null; - ShowAboutCommand = null; - ShowSettingsCommand = null; Closed?.Invoke(this, e); } @@ -214,8 +185,6 @@ namespace FluentTerminal.App.ViewModels } public RelayCommand AddLocalShellCommand { get; private set; } - public RelayCommand AddSshShellCommand { get; private set; } - public RelayCommand AddQuickShellCommand { get; private set; } public string WindowTitle { @@ -279,10 +248,6 @@ namespace FluentTerminal.App.ViewModels } } - public IAsyncCommand ShowAboutCommand { get; private set; } - - public RelayCommand ShowSettingsCommand { get; private set; } - public TabsPosition TabsPosition { get => _tabsPosition; @@ -374,6 +339,9 @@ namespace FluentTerminal.App.ViewModels #endregion For serialization + private Task AddProfileAsync(ShellProfile profile) => + AddProfileAsync(profile, _settingsService.GetApplicationSettings().NewTerminalLocation); + private Task AddProfileAsync(ShellProfile profile, NewTerminalLocation location) { if (profile == null) @@ -597,6 +565,20 @@ namespace FluentTerminal.App.ViewModels }); } + private void OnKeyBindingChanged(KeyBindingsChangedMessage message) + { + LoadKeyBindings(); + + // Should be scheduled no matter if we're in the UI thread. + ApplicationView.RunOnDispatcherThread(CreateMenuViewModel); + } + + private void OnCommandHistoryChanged(CommandHistoryChangedMessage message) + { + // Should be scheduled no matter if we're in the UI thread. + ApplicationView.RunOnDispatcherThread(CreateMenuViewModel); + } + private void SelectTabNumber(int tabNumber) { if (tabNumber < Terminals.Count) @@ -619,12 +601,7 @@ namespace FluentTerminal.App.ViewModels SelectedTerminal = Terminals[previousIndex]; } - private Task ShowAbout() - { - return _dialogService.ShowAboutDialogAsync(); - } - - private void ShowSettings() + public void ShowSettings() { ShowSettingsRequested?.Invoke(this, EventArgs.Empty); } @@ -636,7 +613,7 @@ namespace FluentTerminal.App.ViewModels public void OnWindowKeyDown(int key, bool control, bool alt, bool shift, bool meta) { - var binding = _keyBindings.FirstOrDefault(b => + var binding = _keyBindings.Values.SelectMany(bs => bs).FirstOrDefault(b => b.Key == key && b.Ctrl == control && b.Alt == alt && b.Shift == shift && b.Meta == meta); if (binding != null) @@ -644,5 +621,166 @@ namespace FluentTerminal.App.ViewModels _keyboardCommandService.SendCommand(binding.Command); } } + + #region App menu + + private const int RecentItemsMaxCount = 10; + + private AppMenuViewModel _menuViewModel; + + private RelayCommand _newRemoteTabCommand; + private RelayCommand _newQuickTabCommand; + private RelayCommand _settingsCommand; + private RelayCommand _aboutCommand; + + public AppMenuViewModel MenuViewModel + { + get => _menuViewModel; + private set + { + // Changing app menu view model only if it's actually different than previous. + if (!value.EquivalentTo(_menuViewModel)) + { + Set(ref _menuViewModel, value); + } + } + } + + private void InitializeAppMenu() + { + _newRemoteTabCommand = new RelayCommand(async () => await AddSshProfileAsync(NewTerminalLocation.Tab)); + _newQuickTabCommand = new RelayCommand(async () => await AddQuickLaunchProfileAsync(NewTerminalLocation.Tab)); + _settingsCommand = new RelayCommand(ShowSettings); + _aboutCommand = new RelayCommand(async () => await _dialogService.ShowAboutDialogAsync()); + + CreateMenuViewModel(); + } + + private void CreateMenuViewModel() + { + var tabItem = new MenuItemViewModel(I18N.TranslateWithFallback("NewTab.Text", "New tab"), + AddLocalShellCommand, + I18N.TranslateWithFallback("NewTab_Description", "Opens default profile in a new tab."), + icon: 57609 /*(int) Symbol.Add*/); + + if (_keyBindings.TryGetValue(nameof(Command.NewTab), out var keyBindings) && + keyBindings.FirstOrDefault() is KeyBinding tabKeyBindings && + _acceleratorKeyValidator.Valid(tabKeyBindings.Key)) + { + LoadKeyBindingsFromModel(tabItem, tabKeyBindings); + } + else + { + tabItem.KeyBinding = null; + } + + var remoteTabItem = new MenuItemViewModel(I18N.TranslateWithFallback("NewSshTab.Text", "New remote tab"), + _newRemoteTabCommand, + I18N.TranslateWithFallback("NewSshTab_Description", "Opens a new SSH or Mosh session in a new tab."), + icon: 57609 /*(int) Symbol.Add*/); + + if (_keyBindings.TryGetValue(nameof(Command.NewSshTab), out keyBindings) && + keyBindings.FirstOrDefault() is KeyBinding remoteTabKeyBinding && + _acceleratorKeyValidator.Valid(remoteTabKeyBinding.Key)) + { + LoadKeyBindingsFromModel(remoteTabItem, remoteTabKeyBinding); + } + else + { + remoteTabItem.KeyBinding = null; + } + + var quickTab = new MenuItemViewModel(I18N.TranslateWithFallback("NewQuickTab.Text", "New quick tab"), + _newQuickTabCommand, + I18N.TranslateWithFallback("NewQuickTab_Description", + "Opens \"Quick Launch\" dialog and starts session in a new tab."), + icon: 57609 /*(int) Symbol.Add*/); + + if (_keyBindings.TryGetValue(nameof(Command.NewCustomCommandTab), out keyBindings) && + keyBindings.FirstOrDefault() is KeyBinding quickTabKeyBinding && + _acceleratorKeyValidator.Valid(quickTabKeyBinding.Key)) + { + LoadKeyBindingsFromModel(quickTab, quickTabKeyBinding); + } + else + { + quickTab.KeyBinding = null; + } + + var settings = new MenuItemViewModel(I18N.TranslateWithFallback("Settings.Text", "Settings"), + _settingsCommand, I18N.TranslateWithFallback("Settings_Description", "Opens settings window."), + icon: 57621 /*(int) Symbol.Setting*/); + + if (_keyBindings.TryGetValue(nameof(Command.ShowSettings), out keyBindings) && + keyBindings.FirstOrDefault() is KeyBinding settingsKeyBinding && + _acceleratorKeyValidator.Valid(settingsKeyBinding.Key)) + { + LoadKeyBindingsFromModel(settings, settingsKeyBinding); + } + else + { + settings.KeyBinding = null; + } + + var recent = new ExpandableMenuItemViewModel(I18N.TranslateWithFallback("Recent.Text", "Recent"), + GetRecentMenuItems(), I18N.TranslateWithFallback("Recent_Description", "Recently opened sessions."), + icon: "\uF738" /*Segoe MDL2 Assets Glyph property*/); + + var about = new MenuItemViewModel(I18N.TranslateWithFallback("AboutDialog.Title", "About"), _aboutCommand, + I18N.TranslateWithFallback("About_Description", "Basic info about the app."), + icon: "\uE946" /*Segoe MDL2 Assets Glyph property*/); + + var appMenuViewModel = new AppMenuViewModel(new MenuItemViewModelBase[] + { + tabItem, + remoteTabItem, + quickTab, + settings, + recent, + about, + }); + + MenuViewModel = appMenuViewModel; + } + + private ObservableCollection GetRecentMenuItems() => + new ObservableCollection(_commandHistoryService + .GetHistoryRecentFirst(top: RecentItemsMaxCount).Select(CommandToMenuItem)); + + private MenuItemViewModel CommandToMenuItem(ExecutedCommand command) + { + var itemCommand = new RelayCommand(async () => await AddProfileAsync(command.ShellProfile), + keepTargetAlive: true); + var keyBinding = command.ShellProfile.KeyBindings?.FirstOrDefault() is KeyBinding kb + ? new MenuItemKeyBindingViewModel(kb) + : null; + + return new MenuItemViewModel(command.Value, itemCommand, keyBinding: keyBinding); + } + + private void LoadKeyBindingsFromModel(MenuItemViewModel menuItemViewModel, KeyBinding keyBinding) + { + if (!_acceleratorKeyValidator.Valid(keyBinding.Key)) + { + menuItemViewModel.KeyBinding = null; + + return; + } + + if (menuItemViewModel.KeyBinding == null) + { + menuItemViewModel.KeyBinding = new MenuItemKeyBindingViewModel(keyBinding); + } + else + { + menuItemViewModel.KeyBinding.Key = keyBinding.Key; + menuItemViewModel.KeyBinding.Ctrl = keyBinding.Ctrl; + menuItemViewModel.KeyBinding.Alt = keyBinding.Alt; + menuItemViewModel.KeyBinding.Shift = keyBinding.Shift; + menuItemViewModel.KeyBinding.Windows = keyBinding.Meta; + } + } + + #endregion App menu } } \ No newline at end of file diff --git a/FluentTerminal.App.ViewModels/Menu/AppMenuViewModel.cs b/FluentTerminal.App.ViewModels/Menu/AppMenuViewModel.cs new file mode 100644 index 0000000..60a84c5 --- /dev/null +++ b/FluentTerminal.App.ViewModels/Menu/AppMenuViewModel.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using GalaSoft.MvvmLight; + +namespace FluentTerminal.App.ViewModels.Menu +{ + public class AppMenuViewModel : ViewModelBase + { + public ObservableCollection Items { get; } + + public AppMenuViewModel(IEnumerable items = null) + { + if (items == null) + { + Items = new ObservableCollection(); + } + else if (items is ObservableCollection observableItems) + { + Items = observableItems; + } + else + { + Items = new ObservableCollection(items); + } + } + + public bool EquivalentTo(AppMenuViewModel other) + { + if (ReferenceEquals(this, other)) return true; + + if (other == null) return false; + + if (Items.Count != other.Items.Count) return false; + + return !Items.Where((t, i) => !t.EquivalentTo(other.Items[i])).Any(); + } + } +} \ No newline at end of file diff --git a/FluentTerminal.App.ViewModels/Menu/ExpandableMenuItemViewModel.cs b/FluentTerminal.App.ViewModels/Menu/ExpandableMenuItemViewModel.cs new file mode 100644 index 0000000..c057e75 --- /dev/null +++ b/FluentTerminal.App.ViewModels/Menu/ExpandableMenuItemViewModel.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace FluentTerminal.App.ViewModels.Menu +{ + public class ExpandableMenuItemViewModel : MenuItemViewModelBase + { + public ObservableCollection SubItems { get; } + + public ExpandableMenuItemViewModel(string text, IEnumerable subItems = null, + string description = null, object icon = null) : base(text, description, icon) + { + if (subItems == null) + { + SubItems = new ObservableCollection(); + } + else if (subItems is ObservableCollection observableSubItems) + { + SubItems = observableSubItems; + } + else + { + SubItems = new ObservableCollection(subItems); + } + } + + public override bool EquivalentTo(MenuItemViewModelBase other) + { + if (!base.EquivalentTo(other)) return false; + + if (!(other is ExpandableMenuItemViewModel otherExpandable) || + SubItems.Count != otherExpandable.SubItems.Count) + { + return false; + } + + return !SubItems.Where((t, i) => !t.EquivalentTo(otherExpandable.SubItems[i])).Any(); + } + } +} \ No newline at end of file diff --git a/FluentTerminal.App.ViewModels/Menu/MenuItemKeyBindingViewModel.cs b/FluentTerminal.App.ViewModels/Menu/MenuItemKeyBindingViewModel.cs new file mode 100644 index 0000000..781cb16 --- /dev/null +++ b/FluentTerminal.App.ViewModels/Menu/MenuItemKeyBindingViewModel.cs @@ -0,0 +1,147 @@ +using FluentTerminal.Models; +using GalaSoft.MvvmLight; + +namespace FluentTerminal.App.ViewModels.Menu +{ + public class MenuItemKeyBindingViewModel : ViewModelBase + { + #region Properties + + private int _key; + + public int Key + { + get => _key; + set => Set(ref _key, value); + } + + private int _keyModifiers; + + // Corresponds to https://docs.microsoft.com/en-us/uwp/api/windows.system.virtualkeymodifiers + public int KeyModifiers + { + get => _keyModifiers; + private set => Set(ref _keyModifiers, value); + } + + private bool _ctrl; + + public bool Ctrl + { + get => _ctrl; + set + { + if (Set(ref _ctrl, value)) + { + SetKeyModifiers(); + } + } + } + + private bool _alt; + + public bool Alt + { + get => _alt; + set + { + if (Set(ref _alt, value)) + { + SetKeyModifiers(); + } + } + } + + private bool _shift; + + public bool Shift + { + get => _shift; + set + { + if (Set(ref _shift, value)) + { + SetKeyModifiers(); + } + } + } + + private bool _windows; + + public bool Windows + { + get => _windows; + set + { + if (Set(ref _windows, value)) + { + SetKeyModifiers(); + } + } + } + + #endregion Properties + + #region Constructors + + public MenuItemKeyBindingViewModel(int key = 0, bool ctrl = false, bool alt = false, bool shift = false, + bool windows = false) + { + _key = key; + _ctrl = ctrl; + _alt = alt; + _shift = shift; + _windows = windows; + + SetKeyModifiers(); + } + + public MenuItemKeyBindingViewModel(KeyBinding keyBinding) : this(keyBinding.Key, keyBinding.Ctrl, + keyBinding.Alt, keyBinding.Shift, keyBinding.Meta) + { + } + + #endregion Constructors + + #region Methods + + private void SetKeyModifiers() + { + // Based on VirtualKeyModifiers enum values (https://docs.microsoft.com/en-us/uwp/api/windows.system.virtualkeymodifiers) + var result = 0; + + if (_ctrl) + { + result |= 1; + } + + if (_alt) + { + result |= 2; + } + + if (_shift) + { + result |= 4; + } + + if (_windows) + { + result |= 8; + } + + KeyModifiers = result; + } + + public bool EquivalentTo(MenuItemKeyBindingViewModel other) + { + if (ReferenceEquals(this, other)) return true; + + if (other == null) return false; + + return _key == other._key && _keyModifiers == other._keyModifiers; + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/FluentTerminal.App.ViewModels/Menu/MenuItemViewModel.cs b/FluentTerminal.App.ViewModels/Menu/MenuItemViewModel.cs new file mode 100644 index 0000000..7941365 --- /dev/null +++ b/FluentTerminal.App.ViewModels/Menu/MenuItemViewModel.cs @@ -0,0 +1,46 @@ +using System; +using GalaSoft.MvvmLight.Command; + +namespace FluentTerminal.App.ViewModels.Menu +{ + public class MenuItemViewModel : MenuItemViewModelBase + { + #region Properties + + private MenuItemKeyBindingViewModel _keyBinding; + + public MenuItemKeyBindingViewModel KeyBinding + { + get => _keyBinding; + set => Set(ref _keyBinding, value); + } + + public RelayCommand Command { get; } + + #endregion Properties + + #region Constructors + + public MenuItemViewModel(string text, RelayCommand command, string description = null, object icon = null, + MenuItemKeyBindingViewModel keyBinding = null) : base(text, description, icon) + { + _keyBinding = keyBinding; + Command = command ?? throw new ArgumentNullException(nameof(command)); + } + + #endregion Constructors + + #region Methods + + public override bool EquivalentTo(MenuItemViewModelBase other) + { + if (!base.EquivalentTo(other)) return false; + + if (!(other is MenuItemViewModel menuItem)) return false; + + return _keyBinding?.EquivalentTo(menuItem._keyBinding) ?? menuItem._keyBinding == null; + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/FluentTerminal.App.ViewModels/Menu/MenuItemViewModelBase.cs b/FluentTerminal.App.ViewModels/Menu/MenuItemViewModelBase.cs new file mode 100644 index 0000000..3c8566c --- /dev/null +++ b/FluentTerminal.App.ViewModels/Menu/MenuItemViewModelBase.cs @@ -0,0 +1,84 @@ +using FluentTerminal.Models; +using GalaSoft.MvvmLight; + +namespace FluentTerminal.App.ViewModels.Menu +{ + public class MenuItemViewModelBase : ViewModelBase + { + #region Properties + + private string _text; + + public string Text + { + get => _text; + set => Set(ref _text, value); + } + + private string _description; + + public string Description + { + get => _description; + set => Set(ref _description, value); + } + + private object _icon; + + /// + /// Defines menu item icon. + /// + /// + /// Acceptable values of this property are: + /// + /// IconElement + /// (usually SymbolIcon + /// or FontIcon); + /// Symbol enum value, + /// which will be used for creating + /// SymbolIcon; + /// , in which case it'll be converted to + /// Symbol enum value; + /// , in which case a new + /// FontIcon instance will + /// be created, whose + /// Glyph property + /// will be set to the value of this property. + /// FontFamily + /// property of the newly created instance will be set to "Segoe MDL2 Assets". + /// + /// + public object Icon + { + get => _icon; + set => Set(ref _icon, value); + } + + #endregion Properties + + #region Constructor + + protected MenuItemViewModelBase(string text, string description, object icon) + { + _text = text; + _description = description; + _icon = icon; + } + + #endregion Constructor + + #region Methods + + public virtual bool EquivalentTo(MenuItemViewModelBase other) + { + if (ReferenceEquals(this, other)) return true; + + if (other == null) return false; + + return _text.NullableEqualTo(other._text) && _description.NullableEqualTo(other._description) && + (_icon?.Equals(other._icon) ?? other._icon == null); + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/FluentTerminal.App.ViewModels/ProfileCommandViewModel.cs b/FluentTerminal.App.ViewModels/ProfileCommandViewModel.cs deleted file mode 100644 index ccc7fbc..0000000 --- a/FluentTerminal.App.ViewModels/ProfileCommandViewModel.cs +++ /dev/null @@ -1,19 +0,0 @@ -using FluentTerminal.Models; -using GalaSoft.MvvmLight; -using GalaSoft.MvvmLight.Command; - -namespace FluentTerminal.App.ViewModels -{ - public class ProfileCommandViewModel : ViewModelBase - { - public ShellProfile Profile { get; } - - public RelayCommand Command { get; } - - public ProfileCommandViewModel(ShellProfile profile, RelayCommand command) - { - Profile = profile; - Command = command; - } - } -} \ No newline at end of file diff --git a/FluentTerminal.App/App.xaml.cs b/FluentTerminal.App/App.xaml.cs index 377cca2..ed6bddb 100755 --- a/FluentTerminal.App/App.xaml.cs +++ b/FluentTerminal.App/App.xaml.cs @@ -113,6 +113,7 @@ namespace FluentTerminal.App builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); _container = builder.Build(); diff --git a/FluentTerminal.App/Converters/AppMenuViewModelToFlyoutMenuConverter.cs b/FluentTerminal.App/Converters/AppMenuViewModelToFlyoutMenuConverter.cs new file mode 100644 index 0000000..22db7a3 --- /dev/null +++ b/FluentTerminal.App/Converters/AppMenuViewModelToFlyoutMenuConverter.cs @@ -0,0 +1,39 @@ +using System; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Data; +using FluentTerminal.App.ViewModels.Menu; + +namespace FluentTerminal.App.Converters +{ + public class AppMenuViewModelToFlyoutMenuConverter : IValueConverter + { + private static readonly MenuItemViewModelBaseToMenuFlayoutItemBaseConverter ItemConverter = + new MenuItemViewModelBaseToMenuFlayoutItemBaseConverter(); + + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is null) + { + return null; + } + + if (!(value is AppMenuViewModel appMenuViewModel)) + { + throw new ArgumentException( + $"Invalid {nameof(value)} argument type: {value.GetType()}. {typeof(AppMenuViewModel)} expected."); + } + + var menuFlyout = new MenuFlyout(); + + foreach (var menuItemViewModelBase in appMenuViewModel.Items) + { + menuFlyout.Items?.Add((MenuFlyoutItemBase) ItemConverter.Convert(menuItemViewModelBase)); + } + + return menuFlyout; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) => + throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/FluentTerminal.App/Converters/IconConverter.cs b/FluentTerminal.App/Converters/IconConverter.cs new file mode 100644 index 0000000..24bf9eb --- /dev/null +++ b/FluentTerminal.App/Converters/IconConverter.cs @@ -0,0 +1,49 @@ +using System; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Media; +// ReSharper disable LocalizableElement + +namespace FluentTerminal.App.Converters +{ + public class IconConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value == null) + { + return null; + } + + if (value is IconElement icon) + { + return icon; + } + + if (value is Symbol symbol) + { + return new SymbolIcon(symbol); + } + + if (value is int intSymbol) + { + if (Enum.IsDefined(typeof(Symbol), intSymbol)) + { + return new SymbolIcon((Symbol) intSymbol); + } + + throw new ArgumentException($"Integer value isn't defined in {nameof(Symbol)} enum.", nameof(value)); + } + + if (value is string glyph) + { + return new FontIcon {FontFamily = new FontFamily("Segoe MDL2 Assets"), Glyph = glyph}; + } + + throw new ArgumentException($"Invalid {nameof(value)} parameter type: {value.GetType()}.", nameof(value)); + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) => + throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/FluentTerminal.App/Converters/MenuItemViewModelBaseToMenuFlayoutItemBaseConverter.cs b/FluentTerminal.App/Converters/MenuItemViewModelBaseToMenuFlayoutItemBaseConverter.cs new file mode 100644 index 0000000..d08744b --- /dev/null +++ b/FluentTerminal.App/Converters/MenuItemViewModelBaseToMenuFlayoutItemBaseConverter.cs @@ -0,0 +1,131 @@ +using System; +using Windows.System; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using FluentTerminal.App.ViewModels.Menu; +using FluentTerminal.App.Views; + +// ReSharper disable LocalizableElement + +namespace FluentTerminal.App.Converters +{ + public class MenuItemViewModelBaseToMenuFlayoutItemBaseConverter : IValueConverter + { + #region Static + + private static readonly IconConverter IconConverter = new IconConverter(); + + private static MenuFlyoutItemBase GetItem(MenuItemViewModelBase viewModel) => + viewModel is ExpandableMenuItemViewModel expandableMenuItemViewModel + ? (MenuFlyoutItemBase)GetExpandableItem(expandableMenuItemViewModel) + : GetRegularItem((MenuItemViewModel)viewModel); + + private static MenuFlyoutItem GetRegularItem(MenuItemViewModel viewModel) + { + var item = new MenuFlyoutItem(); + + item.SetBinding(MenuFlyoutItem.TextProperty, new Binding + { + Source = viewModel, + Path = new PropertyPath(nameof(MenuItemViewModelBase.Text)), + Mode = BindingMode.OneWay + }); + + item.SetBinding(ToolTipService.ToolTipProperty, new Binding + { + Source = viewModel, + Path = new PropertyPath(nameof(MenuItemViewModelBase.Description)), + Mode = BindingMode.OneWay + }); + + item.SetBinding(MenuFlyoutItem.IconProperty, new Binding + { + Source = viewModel, + Path = new PropertyPath(nameof(MenuItemViewModelBase.Icon)), + Converter = IconConverter, + Mode = BindingMode.OneWay + }); + + item.SetBinding(MenuFlyoutItem.CommandProperty, new Binding + { + Source = viewModel, + Path = new PropertyPath(nameof(MenuItemViewModel.Command)), + Mode = BindingMode.OneTime + }); + + if (viewModel.KeyBinding is MenuItemKeyBindingViewModel keyBinding) + { + item.KeyboardAccelerators?.Add(new KeyboardAccelerator + { + Key = (VirtualKey)keyBinding.Key, + Modifiers = (VirtualKeyModifiers)keyBinding.KeyModifiers, + IsEnabled = true + }); + } + + return item; + } + + private static MenuFlyoutSubItem GetExpandableItem(ExpandableMenuItemViewModel viewModel) + { + var item = new MenuFlyoutSubItem(); + + item.SetBinding(MenuFlyoutSubItem.TextProperty, new Binding + { + Source = viewModel, + Path = new PropertyPath(nameof(MenuItemViewModelBase.Text)), + Mode = BindingMode.OneWay + }); + + item.SetBinding(ToolTipService.ToolTipProperty, new Binding + { + Source = viewModel, + Path = new PropertyPath(nameof(MenuItemViewModelBase.Description)), + Mode = BindingMode.OneWay + }); + + item.SetBinding(MenuFlyoutSubItem.IconProperty, new Binding + { + Source = viewModel, + Path = new PropertyPath(nameof(MenuItemViewModelBase.Icon)), + Converter = IconConverter, + Mode = BindingMode.OneWay + }); + + item.SetBinding(MenuExtension.SubItemsProperty, new Binding + { + Source = viewModel, + Path = new PropertyPath(nameof(ExpandableMenuItemViewModel.SubItems)), + Mode = BindingMode.OneWay + }); + + return item; + } + + #endregion Static + + #region IValueConverter + + // ReSharper disable once AssignNullToNotNullAttribute + public object Convert(object value, Type targetType = null, object parameter = null, string language = null) + { + if (value == null) + { + return null; + } + + return value is MenuItemViewModelBase menuItemViewModel + ? GetItem(menuItemViewModel) + : throw new ArgumentException( + $"Invalid {nameof(value)} argument type: {value.GetType()}. {typeof(MenuItemViewModelBase)} expected.", + nameof(value)); + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) => + throw new NotSupportedException(); + + #endregion IValueConverter + } +} \ No newline at end of file diff --git a/FluentTerminal.App/Dialogs/CreateKeyBindingDialog.xaml.cs b/FluentTerminal.App/Dialogs/CreateKeyBindingDialog.xaml.cs index c6e1184..dd0c083 100644 --- a/FluentTerminal.App/Dialogs/CreateKeyBindingDialog.xaml.cs +++ b/FluentTerminal.App/Dialogs/CreateKeyBindingDialog.xaml.cs @@ -9,11 +9,14 @@ using System.Threading.Tasks; using Windows.System; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; +using FluentTerminal.App.Services; namespace FluentTerminal.App.Dialogs { public sealed partial class CreateKeyBindingDialog : ContentDialog, ICreateKeyBindingDialog, INotifyPropertyChanged { + private readonly IAcceleratorKeyValidator _acceleratorKeyValidator; + private bool _ctrl; private bool _shift; private bool _alt; @@ -47,11 +50,19 @@ namespace FluentTerminal.App.Dialogs public int Key { get => _key; - set => Set(ref _key, value); + set + { + // We're ignoring invalid keys. + if (_acceleratorKeyValidator.Valid(value)) + { + Set(ref _key, value); + } + } } - public CreateKeyBindingDialog() + public CreateKeyBindingDialog(IAcceleratorKeyValidator acceleratorKeyValidator) { + _acceleratorKeyValidator = acceleratorKeyValidator; InitializeComponent(); ResetCommand = new RelayCommand(Reset); PreviewKeyDown += RegisterKeyBindingDialog_PreviewKeyDown; diff --git a/FluentTerminal.App/FluentTerminal.App.csproj b/FluentTerminal.App/FluentTerminal.App.csproj index 1f2ad05..840d7d5 100644 --- a/FluentTerminal.App/FluentTerminal.App.csproj +++ b/FluentTerminal.App/FluentTerminal.App.csproj @@ -106,9 +106,12 @@ + + + @@ -126,6 +129,7 @@ AboutDialog.xaml + diff --git a/FluentTerminal.App/Services/AcceleratorKeyValidator.cs b/FluentTerminal.App/Services/AcceleratorKeyValidator.cs new file mode 100644 index 0000000..26ec37c --- /dev/null +++ b/FluentTerminal.App/Services/AcceleratorKeyValidator.cs @@ -0,0 +1,13 @@ +using System; +using Windows.System; +using FluentTerminal.Models.Enums; + +namespace FluentTerminal.App.Services +{ + public class AcceleratorKeyValidator : IAcceleratorKeyValidator + { + public bool Valid(int key) => Enum.IsDefined(typeof(VirtualKey), key); + + public bool Valid(ExtendedVirtualKey key) => Valid((int) key); + } +} \ No newline at end of file diff --git a/FluentTerminal.App/Services/CommandHistoryService.cs b/FluentTerminal.App/Services/CommandHistoryService.cs index 52fa127..d710e59 100644 --- a/FluentTerminal.App/Services/CommandHistoryService.cs +++ b/FluentTerminal.App/Services/CommandHistoryService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; @@ -128,7 +128,7 @@ namespace FluentTerminal.App.Services if (ReferenceEquals(command.ShellProfile, newProfile)) continue; - if (!command.IsProfile) + if (!command.ProfileId.HasValue) { newProfile.Name = $"{newProfile.Location} {newProfile.Arguments}".Trim(); } @@ -136,7 +136,7 @@ namespace FluentTerminal.App.Services var newCommand = new ExecutedCommand { Value = newProfile.Name, - IsProfile = command.IsProfile, + ProfileId = command.ProfileId, LastExecution = command.LastExecution, ExecutionCount = command.ExecutionCount, ShellProfile = newProfile @@ -182,14 +182,36 @@ namespace FluentTerminal.App.Services { var command = _history[i]; - if ((command.IsProfile || command.ShellProfile == null) && !profiles.Any(p => - string.Equals(p.Name, command.Value, StringComparison.OrdinalIgnoreCase))) + if (command.ProfileId.HasValue && !profiles.Any(p => p.Id.Equals(command.ProfileId.Value))) { _history.Remove(command); Delete(command); i--; + + continue; + } + + if (command.ShellProfile == null) + { + var profile = profiles.FirstOrDefault(p => + string.Equals(p.Name, command.Value, StringComparison.OrdinalIgnoreCase)); + + if (profile == null) + { + _history.Remove(command); + + Delete(command); + + i--; + } + else + { + command.ProfileId = profile.Id; + + Save(command); + } } } } @@ -212,10 +234,9 @@ namespace FluentTerminal.App.Services foreach (var command in ordered) { - if (command.IsProfile || command.ShellProfile == null) + if (command.ProfileId.HasValue) { - var profile = allProfiles.Value.FirstOrDefault(p => - string.Equals(p.Name, command.Value, StringComparison.OrdinalIgnoreCase)); + var profile = allProfiles.Value.FirstOrDefault(p => p.Id.Equals(command.ProfileId.Value)); if (profile == null) { @@ -250,9 +271,9 @@ namespace FluentTerminal.App.Services yield return new ExecutedCommand { Value = unusedProfile.Name, + ProfileId = unusedProfile.Id, ExecutionCount = 0, LastExecution = NeverUsedTime, - IsProfile = true, ShellProfile = unusedProfile }; } @@ -273,7 +294,8 @@ namespace FluentTerminal.App.Services var history = GetRawHistory(); var existing = history.FirstOrDefault(c => - string.Equals(c.Value, profile.Name, StringComparison.OrdinalIgnoreCase)); + c.ProfileId?.Equals(profile.Id) ?? + string.Equals(profile.Name, c.Value, StringComparison.OrdinalIgnoreCase)); if (existing == null) { @@ -282,14 +304,13 @@ namespace FluentTerminal.App.Services profile.Name = $"{profile.Location} {profile.Arguments}".Trim(); } - existing = new ExecutedCommand - { - Value = profile.Name, - IsProfile = GetAllProfiles().Any(p => - string.Equals(p.Name, profile.Name, StringComparison.OrdinalIgnoreCase)) - }; + existing = new ExecutedCommand {Value = profile.Name}; - if (!existing.IsProfile) + if (GetAllProfiles().Any(p => p.Id.Equals(profile.Id))) + { + existing.ProfileId = profile.Id; + } + else { existing.ShellProfile = profile; } diff --git a/FluentTerminal.App/Strings/en/Resources.resw b/FluentTerminal.App/Strings/en/Resources.resw index 1604474..ff9b4bd 100644 --- a/FluentTerminal.App/Strings/en/Resources.resw +++ b/FluentTerminal.App/Strings/en/Resources.resw @@ -1035,7 +1035,25 @@ Fuzzy Word separator characters Fuzzy - + Recent + + Opens default profile in a new tab. + + + Opens a new SSH or Mosh session in a new tab. + + + Opens "Quick Launch" dialog and starts session in a new tab. + + + Opens settings window. + + + Basic info about the app. + + + Recently opened sessions. + \ No newline at end of file diff --git a/FluentTerminal.App/Views/MainPage.xaml b/FluentTerminal.App/Views/MainPage.xaml index a33f86d..e44e132 100644 --- a/FluentTerminal.App/Views/MainPage.xaml +++ b/FluentTerminal.App/Views/MainPage.xaml @@ -11,6 +11,7 @@ KeyDown="MainPage_OnKeyDown"> + @@ -35,51 +36,13 @@ - coreTitleBar.Height; + public double CoreTitleBarHeight => _coreTitleBar.Height; public TimeSpan NoDuration => TimeSpan.Zero; @@ -37,11 +35,11 @@ namespace FluentTerminal.App.Views { if (FlowDirection == FlowDirection.LeftToRight) { - return new Thickness { Left = coreTitleBar.SystemOverlayLeftInset, Right = coreTitleBar.SystemOverlayRightInset }; + return new Thickness { Left = _coreTitleBar.SystemOverlayLeftInset, Right = _coreTitleBar.SystemOverlayRightInset }; } else { - return new Thickness { Left = coreTitleBar.SystemOverlayRightInset, Right = coreTitleBar.SystemOverlayLeftInset }; + return new Thickness { Left = _coreTitleBar.SystemOverlayRightInset, Right = _coreTitleBar.SystemOverlayLeftInset }; } } } @@ -70,7 +68,7 @@ namespace FluentTerminal.App.Views private async void MainPage_DraggingHappensChanged(object sender, bool e) { - await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => + await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { if (sender != TopTabBar && sender != BottomTabBar) { @@ -111,14 +109,14 @@ namespace FluentTerminal.App.Views DraggingHappensChanged -= MainPage_DraggingHappensChanged; Window.Current.Activated -= OnWindowActivated; - coreTitleBar.LayoutMetricsChanged -= OnLayoutMetricsChanged; + _coreTitleBar.LayoutMetricsChanged -= OnLayoutMetricsChanged; ViewModel.Closed -= ViewModel_Closed; ViewModel = null; Root.DataContext = null; Window.Current.SetTitleBar(null); - coreTitleBar = null; + _coreTitleBar = null; Bindings.StopTracking(); TerminalContainer.Content = null; } @@ -134,7 +132,7 @@ namespace FluentTerminal.App.Views private void OnLoaded(object sender, RoutedEventArgs e) { - coreTitleBar.LayoutMetricsChanged += OnLayoutMetricsChanged; + _coreTitleBar.LayoutMetricsChanged += OnLayoutMetricsChanged; UpdateLayoutMetrics(); } @@ -207,8 +205,11 @@ namespace FluentTerminal.App.Views { Logger.Instance.Debug("TabDropArea_DragEnter."); e.AcceptedOperation = DataPackageOperation.Move; - e.DragUIOverride.IsGlyphVisible = false; - e.DragUIOverride.Caption = I18N.Translate("DropTabHere"); + if (e.DragUIOverride is DragUIOverride dragUiOverride) + { + dragUiOverride.IsGlyphVisible = false; + dragUiOverride.Caption = I18N.Translate("DropTabHere"); + } } private async void TabDropArea_Drop(object sender, DragEventArgs e) diff --git a/FluentTerminal.App/Views/MenuExtension.cs b/FluentTerminal.App/Views/MenuExtension.cs index 9e8501a..eaa5a69 100644 --- a/FluentTerminal.App/Views/MenuExtension.cs +++ b/FluentTerminal.App/Views/MenuExtension.cs @@ -1,38 +1,38 @@ using System.Collections.Generic; +using System.Linq; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; -using FluentTerminal.App.ViewModels; +using FluentTerminal.App.Converters; +using FluentTerminal.App.ViewModels.Menu; namespace FluentTerminal.App.Views { // https://stackoverflow.com/questions/33839279/bind-obserable-collection-to-menuflyoutsubitem-in-uwp public static class MenuExtension { - public static IEnumerable GetSubItems(DependencyObject obj) => - (IEnumerable) obj.GetValue(SubItemsProperty); + private static readonly MenuItemViewModelBaseToMenuFlayoutItemBaseConverter Converter = + new MenuItemViewModelBaseToMenuFlayoutItemBaseConverter(); - public static void SetSubItems(DependencyObject obj, IEnumerable value) => + public static IEnumerable GetSubItems(DependencyObject obj) => + (IEnumerable) obj.GetValue(SubItemsProperty); + + public static void SetSubItems(DependencyObject obj, IEnumerable value) => obj.SetValue(SubItemsProperty, value); public static readonly DependencyProperty SubItemsProperty = - DependencyProperty.Register("SubItems", typeof(IEnumerable), typeof(MenuExtension), - new PropertyMetadata(new List(), (sender, e) => + DependencyProperty.Register("SubItems", typeof(IEnumerable), typeof(MenuExtension), + new PropertyMetadata(new List(), (sender, e) => { - if (sender is MenuFlyoutSubItem subItemsRoot && subItemsRoot.Items != null) + if (sender is MenuFlyoutSubItem menuSubItem) { - subItemsRoot.Items.Clear(); + menuSubItem.Items?.Clear(); - if (e.NewValue is IEnumerable commands) + if (e.NewValue is IEnumerable viewModels) { - foreach (var command in commands) + foreach (var item in viewModels.Select(vm => (MenuFlyoutItemBase) Converter.Convert(vm))) { - subItemsRoot.Items.Add(new MenuFlyoutItem {Text = command.Profile.Name, Command = command.Command}); + menuSubItem.Items?.Add(item); } - - // Hack to enforce changing items in view - //subItemsRoot.UpdateLayout(); - //subItemsRoot.InvalidateArrange(); - subItemsRoot.InvalidateMeasure(); } } })); diff --git a/FluentTerminal.Models/ExecutedCommand.cs b/FluentTerminal.Models/ExecutedCommand.cs index 42ac612..dda4eb0 100644 --- a/FluentTerminal.Models/ExecutedCommand.cs +++ b/FluentTerminal.Models/ExecutedCommand.cs @@ -6,7 +6,7 @@ namespace FluentTerminal.Models { public string Value { get; set; } - public bool IsProfile { get; set; } + public Guid? ProfileId { get; set; } public DateTime LastExecution { get; set; } @@ -16,7 +16,7 @@ namespace FluentTerminal.Models public ExecutedCommand Clone() => new ExecutedCommand { - Value = Value, IsProfile = IsProfile, LastExecution = LastExecution, ExecutionCount = ExecutionCount, + Value = Value, ProfileId = ProfileId, LastExecution = LastExecution, ExecutionCount = ExecutionCount, ShellProfile = ShellProfile?.Clone() }; }