From f86f20321d0f2418df8cfc6aaa824b2071fb8e2f Mon Sep 17 00:00:00 2001 From: Felix Seidl Date: Tue, 3 Mar 2020 18:39:38 +0100 Subject: [PATCH] Rework tab context menu --- .../MainViewModel.cs | 18 +- .../Menu/ExpandableMenuItemViewModel.cs | 2 +- .../Menu/Mdl2Icon.cs | 44 +++++ .../Menu/MenuItemViewModel.cs | 2 +- .../Menu/MenuItemViewModelBase.cs | 29 +--- .../Menu/RadioMenuItemViewModel.cs | 28 ++++ .../Menu/ToggleMenuItemViewModel.cs | 26 +++ .../TabThemeViewModel.cs | 31 ++++ .../TerminalViewModel.cs | 154 +++++++++++++----- .../Converters/IconConverter.cs | 34 +--- ...ModelBaseToMenuFlayoutItemBaseConverter.cs | 90 ++++++++++ .../Converters/TabColorFallbackConverter.cs | 5 +- .../Converters/TabThemeSelectedConverter.cs | 7 +- FluentTerminal.App/Views/TabBar.xaml | 111 +------------ 14 files changed, 369 insertions(+), 212 deletions(-) create mode 100644 FluentTerminal.App.ViewModels/Menu/Mdl2Icon.cs create mode 100644 FluentTerminal.App.ViewModels/Menu/RadioMenuItemViewModel.cs create mode 100644 FluentTerminal.App.ViewModels/Menu/ToggleMenuItemViewModel.cs create mode 100644 FluentTerminal.App.ViewModels/TabThemeViewModel.cs diff --git a/FluentTerminal.App.ViewModels/MainViewModel.cs b/FluentTerminal.App.ViewModels/MainViewModel.cs index f2f1ee6..97f95b6 100644 --- a/FluentTerminal.App.ViewModels/MainViewModel.cs +++ b/FluentTerminal.App.ViewModels/MainViewModel.cs @@ -655,7 +655,7 @@ namespace FluentTerminal.App.ViewModels var tabItem = new ExpandableMenuItemViewModel( I18N.TranslateWithFallback("MenuItem_NewTab_Text", "New Tab"), description: I18N.TranslateWithFallback("MenuItem_NewTab_Text", "Launches a session in a new tab."), - icon: 57609 /*(int) Symbol.Add*/); + icon: Mdl2Icon.Add()); FillCoreItems(tabItem.SubItems, NewTerminalLocation.Tab); @@ -664,7 +664,7 @@ namespace FluentTerminal.App.ViewModels var windowItem = new ExpandableMenuItemViewModel( I18N.TranslateWithFallback("MenuItem_NewWindow_Text", "New Window"), description: I18N.TranslateWithFallback("MenuItem_NewWindow_Description", "Launches a session in a new window."), - icon: "\uE78B" /*Segoe MDL2 Assets Glyph property*/); + icon: Mdl2Icon.NewWindow()); FillCoreItems(windowItem.SubItems, NewTerminalLocation.Window); @@ -677,13 +677,13 @@ namespace FluentTerminal.App.ViewModels appMenuViewModel.Items.Add(new ExpandableMenuItemViewModel(I18N.TranslateWithFallback("Recent.Text", "Recent"), GetRecentMenuItems(), I18N.TranslateWithFallback("Recent_Description", "Recently opened sessions."), - icon: "\uF738" /*Segoe MDL2 Assets Glyph property*/)); + icon: Mdl2Icon.History())); appMenuViewModel.Items.Add(new SeparatorMenuItemViewModel()); var settingsItem = new MenuItemViewModel(I18N.TranslateWithFallback("Settings.Text", "Settings"), _settingsCommand, I18N.TranslateWithFallback("Settings_Description", "Opens settings window."), - icon: 57621 /*(int) Symbol.Setting*/); + icon: Mdl2Icon.Settings()); if (_keyBindings.TryGetValue(nameof(Command.ShowSettings), out var keyBindings) && keyBindings.FirstOrDefault() is KeyBinding settingsKeyBinding && @@ -700,13 +700,13 @@ namespace FluentTerminal.App.ViewModels appMenuViewModel.Items.Add(new MenuItemViewModel(I18N.TranslateWithFallback("AboutDialog.Title", "About"), _aboutCommand, I18N.TranslateWithFallback("About_Description", "Basic info about the app."), - icon: "\uE946" /*Segoe MDL2 Assets Glyph property*/)); + icon: Mdl2Icon.Info())); appMenuViewModel.Items.Add(new SeparatorMenuItemViewModel()); var quitItem = new MenuItemViewModel(I18N.TranslateWithFallback("Quit.Text", "Quit"), _quitCommand, I18N.TranslateWithFallback("Quit.Description", "Quit Fluent Terminal"), - icon: (int)Windows.UI.Xaml.Controls.Symbol.Cancel); + icon: Mdl2Icon.Cancel()); appMenuViewModel.Items.Add(quitItem); if (!appMenuViewModel.EquivalentTo(_menuViewModel)) @@ -724,7 +724,7 @@ namespace FluentTerminal.App.ViewModels var defaultProfileItem = new MenuItemViewModel( defaultProfileName, tab ? _newDefaultTabCommand : _newDefaultWindowCommand, I18N.TranslateWithFallback("MenuItem_DefaultProfile_Description", - "Starts new terminal session based on the default profile."), icon: 57609 /*(int) Symbol.Add*/); + "Starts new terminal session based on the default profile."), icon: Mdl2Icon.Add()); var defaultProfileCommand = tab ? nameof(Command.NewTab) : nameof(Command.NewWindow); @@ -747,7 +747,7 @@ namespace FluentTerminal.App.ViewModels tab ? _newRemoteTabCommand : _newRemoteWindowCommand, I18N.TranslateWithFallback("MenuItem_Remote_Description", "Opens a dialog for launching a new SSH or Mosh terminal session."), - icon: 57609 /*(int) Symbol.Add*/); + icon: Mdl2Icon.Add()); var command = tab ? nameof(Command.NewSshTab) : nameof(Command.NewSshWindow); @@ -769,7 +769,7 @@ namespace FluentTerminal.App.ViewModels tab ? _newQuickLaunchTabCommand : _newQuickLaunchWindowCommand, I18N.TranslateWithFallback("MenuItem_QuickLaunch_Description", "Opens a \"Quick Launch\" dialog for starting a new terminal session."), - icon: 57609 /*(int) Symbol.Add*/); + icon: Mdl2Icon.Add()); command = tab ? nameof(Command.NewCustomCommandTab) : nameof(Command.NewCustomCommandWindow); diff --git a/FluentTerminal.App.ViewModels/Menu/ExpandableMenuItemViewModel.cs b/FluentTerminal.App.ViewModels/Menu/ExpandableMenuItemViewModel.cs index c057e75..8116d39 100644 --- a/FluentTerminal.App.ViewModels/Menu/ExpandableMenuItemViewModel.cs +++ b/FluentTerminal.App.ViewModels/Menu/ExpandableMenuItemViewModel.cs @@ -9,7 +9,7 @@ namespace FluentTerminal.App.ViewModels.Menu public ObservableCollection SubItems { get; } public ExpandableMenuItemViewModel(string text, IEnumerable subItems = null, - string description = null, object icon = null) : base(text, description, icon) + string description = null, Mdl2Icon icon = null) : base(text, description, icon) { if (subItems == null) { diff --git a/FluentTerminal.App.ViewModels/Menu/Mdl2Icon.cs b/FluentTerminal.App.ViewModels/Menu/Mdl2Icon.cs new file mode 100644 index 0000000..08f5eaa --- /dev/null +++ b/FluentTerminal.App.ViewModels/Menu/Mdl2Icon.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace FluentTerminal.App.ViewModels.Menu +{ + public sealed class Mdl2Icon + { + public string Color { get; } + public string Glyph { get; } + + private Mdl2Icon(string glyph, string color = null) + { + Glyph = glyph; + Color = color; + } + + public static Mdl2Icon Link(string color = null) => new Mdl2Icon("\uE71B", color); + + public static Mdl2Icon Copy(string color = null) => new Mdl2Icon("\uE8C8", color); + + public static Mdl2Icon Paste(string color = null) => new Mdl2Icon("\uE77F", color); + + public static Mdl2Icon Edit(string color = null) => new Mdl2Icon("\uE70F", color); + + public static Mdl2Icon Search(string color = null) => new Mdl2Icon("\uE721", color); + + public static Mdl2Icon Add(string color = null) => new Mdl2Icon("\uE710", color); + + public static Mdl2Icon Cancel(string color = null) => new Mdl2Icon("\uE711", color); + + public static Mdl2Icon PaginationDotOutline10(string color = null) => new Mdl2Icon("\uF126", color); + + public static Mdl2Icon PaginationDotSolid10(string color = null) => new Mdl2Icon("\uF127", color); + + public static Mdl2Icon NewWindow(string color = null) => new Mdl2Icon("\uE78B", color); + + public static Mdl2Icon History(string color = null) => new Mdl2Icon("\uE81C", color); + + public static Mdl2Icon Settings(string color = null) => new Mdl2Icon("\uE713", color); + + public static Mdl2Icon Info(string color = null) => new Mdl2Icon("\uE946", color); + } +} diff --git a/FluentTerminal.App.ViewModels/Menu/MenuItemViewModel.cs b/FluentTerminal.App.ViewModels/Menu/MenuItemViewModel.cs index 6d81a4e..4aacf88 100644 --- a/FluentTerminal.App.ViewModels/Menu/MenuItemViewModel.cs +++ b/FluentTerminal.App.ViewModels/Menu/MenuItemViewModel.cs @@ -21,7 +21,7 @@ namespace FluentTerminal.App.ViewModels.Menu #region Constructors - public MenuItemViewModel(string text, ICommand command, string description = null, object icon = null, + public MenuItemViewModel(string text, ICommand command, string description = null, Mdl2Icon icon = null, MenuItemKeyBindingViewModel keyBinding = null) : base(text, description, icon) { _keyBinding = keyBinding; diff --git a/FluentTerminal.App.ViewModels/Menu/MenuItemViewModelBase.cs b/FluentTerminal.App.ViewModels/Menu/MenuItemViewModelBase.cs index 3c8566c..193c42c 100644 --- a/FluentTerminal.App.ViewModels/Menu/MenuItemViewModelBase.cs +++ b/FluentTerminal.App.ViewModels/Menu/MenuItemViewModelBase.cs @@ -23,32 +23,9 @@ namespace FluentTerminal.App.ViewModels.Menu set => Set(ref _description, value); } - private object _icon; + private Mdl2Icon _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 + public Mdl2Icon Icon { get => _icon; set => Set(ref _icon, value); @@ -58,7 +35,7 @@ namespace FluentTerminal.App.ViewModels.Menu #region Constructor - protected MenuItemViewModelBase(string text, string description, object icon) + protected MenuItemViewModelBase(string text, string description, Mdl2Icon icon) { _text = text; _description = description; diff --git a/FluentTerminal.App.ViewModels/Menu/RadioMenuItemViewModel.cs b/FluentTerminal.App.ViewModels/Menu/RadioMenuItemViewModel.cs new file mode 100644 index 0000000..d477165 --- /dev/null +++ b/FluentTerminal.App.ViewModels/Menu/RadioMenuItemViewModel.cs @@ -0,0 +1,28 @@ +using System; + +namespace FluentTerminal.App.ViewModels.Menu +{ + public class RadioMenuItemViewModel : MenuItemViewModelBase + { + public object BindingSource { get; } + public string BindingPath { get; } + public string GroupName { get; } + + public RadioMenuItemViewModel(string text, string groupName, object bindingSource, string bindingPath = null, string description = null, Mdl2Icon icon = null) + : base(text, description, icon) + { + GroupName = groupName ?? throw new ArgumentNullException(nameof(groupName)); + BindingSource = bindingSource ?? throw new ArgumentNullException(nameof(bindingSource)); + BindingPath = bindingPath; + } + + public override bool EquivalentTo(MenuItemViewModelBase other) + { + if (!base.EquivalentTo(other)) return false; + + if (!(other is RadioMenuItemViewModel radioMenuItem)) return false; + + return GroupName == radioMenuItem.GroupName && BindingSource == radioMenuItem.BindingSource && BindingPath == radioMenuItem.BindingPath; + } + } +} diff --git a/FluentTerminal.App.ViewModels/Menu/ToggleMenuItemViewModel.cs b/FluentTerminal.App.ViewModels/Menu/ToggleMenuItemViewModel.cs new file mode 100644 index 0000000..264ab48 --- /dev/null +++ b/FluentTerminal.App.ViewModels/Menu/ToggleMenuItemViewModel.cs @@ -0,0 +1,26 @@ +using System; + +namespace FluentTerminal.App.ViewModels.Menu +{ + public class ToggleMenuItemViewModel : MenuItemViewModelBase + { + public object BindingSource { get; } + public string BindingPath { get; } + + public ToggleMenuItemViewModel(string text, object bindingSource, string bindingPath = null, string description = null, Mdl2Icon icon = null) + : base(text, description, icon) + { + BindingSource = bindingSource ?? throw new ArgumentNullException(nameof(bindingSource)); + BindingPath = bindingPath; + } + + public override bool EquivalentTo(MenuItemViewModelBase other) + { + if (!base.EquivalentTo(other)) return false; + + if (!(other is ToggleMenuItemViewModel toggleMenuItem)) return false; + + return BindingSource == toggleMenuItem.BindingSource && BindingPath == toggleMenuItem.BindingPath; + } + } +} diff --git a/FluentTerminal.App.ViewModels/TabThemeViewModel.cs b/FluentTerminal.App.ViewModels/TabThemeViewModel.cs new file mode 100644 index 0000000..6c33539 --- /dev/null +++ b/FluentTerminal.App.ViewModels/TabThemeViewModel.cs @@ -0,0 +1,31 @@ +using FluentTerminal.Models; +using GalaSoft.MvvmLight; + +namespace FluentTerminal.App.ViewModels +{ + public class TabThemeViewModel : ViewModelBase + { + private bool _isSelected; + + public TabThemeViewModel(TabTheme theme, TerminalViewModel terminal) + { + Theme = theme; + Terminal = terminal; + } + + public bool IsSelected + { + get => _isSelected; + set + { + if (Set(ref _isSelected, value) && value) + { + Terminal.TabTheme = this; + } + } + } + + public TabTheme Theme { get; } + public TerminalViewModel Terminal { get; } + } +} diff --git a/FluentTerminal.App.ViewModels/TerminalViewModel.cs b/FluentTerminal.App.ViewModels/TerminalViewModel.cs index be992df..31c2bcc 100644 --- a/FluentTerminal.App.ViewModels/TerminalViewModel.cs +++ b/FluentTerminal.App.ViewModels/TerminalViewModel.cs @@ -15,6 +15,9 @@ using Windows.UI.Core; using FluentTerminal.Models.Messages; using System.Windows.Input; using FluentTerminal.App.ViewModels.Menu; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml; +using System.Collections.Generic; namespace FluentTerminal.App.ViewModels { @@ -34,7 +37,7 @@ namespace FluentTerminal.App.ViewModels public string ShellTitle { get; set; } public string TabTitle { get; set; } public TerminalTheme TerminalTheme { get; set; } - public TabTheme TabTheme { get; set; } + public int TabThemeId { get; set; } public bool ShowSearchPanel { get; set; } public string SearchText { get; set; } public bool SearchMatchCase { get; set; } @@ -53,7 +56,7 @@ namespace FluentTerminal.App.ViewModels ShellTitle = ShellTitle, TabTitle = TabTitle, TerminalTheme = TerminalTheme, - TabTheme = TabTheme, + TabThemeId = TabTheme.Theme.Id, ShowSearchPanel = ShowSearchPanel, SearchText = SearchText, SearchMatchCase = SearchMatchCase, @@ -76,7 +79,7 @@ namespace FluentTerminal.App.ViewModels ShellTitle = state.ShellTitle; TabTitle = state.TabTitle; TerminalTheme = state.TerminalTheme; - TabTheme = state.TabTheme; + TabTheme = TabThemes.FirstOrDefault(t => t.Theme.Id == state.TabThemeId); ShowSearchPanel = state.ShowSearchPanel; SearchText = state.SearchText; SearchMatchCase = state.SearchMatchCase; @@ -85,6 +88,8 @@ namespace FluentTerminal.App.ViewModels XtermBufferState = state.XtermBufferState; _terminalId = state.TerminalId; ShellProfile = state.ShellProfile; + + TabTheme.IsSelected = true; } } @@ -100,7 +105,7 @@ namespace FluentTerminal.App.ViewModels private bool _searchMatchCase; private bool _searchWholeWord; private bool _searchWithRegex; - private TabTheme _tabTheme; + private TabThemeViewModel _tabTheme; private TerminalTheme _terminalTheme; private TerminalOptions _terminalOptions; private string _tabTitle; @@ -110,6 +115,7 @@ namespace FluentTerminal.App.ViewModels private bool _hasSelection; private string _hoveredUri; private MenuViewModel _contextMenu; + private MenuViewModel _tabContextMenu; public TerminalViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService, IKeyboardCommandService keyboardCommandService, ApplicationSettings applicationSettings, ShellProfile shellProfile, @@ -135,9 +141,7 @@ namespace FluentTerminal.App.ViewModels ShellProfile = shellProfile; TerminalTheme = shellProfile.TerminalThemeId == Guid.Empty ? SettingsService.GetCurrentTheme() : SettingsService.GetTheme(shellProfile.TerminalThemeId); - TabThemes = new ObservableCollection(SettingsService.GetTabThemes()); - TabTheme = TabThemes.FirstOrDefault(t => t.Id == ShellProfile.TabThemeId); - + TabThemes = new ObservableCollection(SettingsService.GetTabThemes().Select(theme => new TabThemeViewModel(theme, this))); CloseCommand = new AsyncCommand(TryCloseAsync, CanExecuteCommand); CloseLeftTabsCommand = new RelayCommand(CloseLeftTabs, CanExecuteCommand); CloseRightTabsCommand = new RelayCommand(CloseRightTabs, CanExecuteCommand); @@ -145,17 +149,22 @@ namespace FluentTerminal.App.ViewModels FindNextCommand = new RelayCommand(FindNext, CanExecuteCommand); FindPreviousCommand = new RelayCommand(FindPrevious, CanExecuteCommand); CloseSearchPanelCommand = new RelayCommand(CloseSearchPanel, CanExecuteCommand); - SelectTabThemeCommand = new RelayCommand(SelectTabTheme, CanExecuteCommand); EditTitleCommand = new AsyncCommand(EditTitleAsync, CanExecuteCommand); DuplicateTabCommand = new RelayCommand(DuplicateTab, CanExecuteCommand); CopyCommand = new AsyncCommand(Copy, () => HasSelection); PasteCommand = new AsyncCommand(Paste); CopyLinkCommand = new AsyncCommand(() => CopyTextAsync(HoveredUri), () => !string.IsNullOrWhiteSpace(HoveredUri)); ShowSearchPanelCommand = new RelayCommand(() => ShowSearchPanel = true, () => !ShowSearchPanel); + if (!string.IsNullOrEmpty(terminalState)) { Restore(terminalState); } + else + { + var defaultTabTheme = TabThemes.FirstOrDefault(t => t.Theme.Id == ShellProfile.TabThemeId); + defaultTabTheme.IsSelected = true; + } Terminal = new Terminal(TrayProcessCommunicationService, _terminalId); Terminal.KeyboardCommandReceived += Terminal_KeyboardCommandReceived; @@ -166,6 +175,7 @@ namespace FluentTerminal.App.ViewModels Terminal.Closed += Terminal_Closed; ContextMenu = BuidContextMenu(); + TabContextMenu = BuildTabContextMenu(); } public MenuViewModel ContextMenu @@ -174,6 +184,12 @@ namespace FluentTerminal.App.ViewModels set => Set(ref _contextMenu, value); } + public MenuViewModel TabContextMenu + { + get => _tabContextMenu; + set => Set(ref _tabContextMenu, value); + } + private bool _disposalRequested; public void DisposalPrepare() @@ -206,7 +222,7 @@ namespace FluentTerminal.App.ViewModels // The effective background theme depends on whether it is selected (use the theme), or if it is inactive // (if we're set to underline inactive tabs, use the null theme). get => IsSelected || (!IsSelected && ApplicationSettings.InactiveTabColorMode == InactiveTabColorMode.Background) ? - _tabTheme : + _tabTheme?.Theme : SettingsService.GetTabThemes().FirstOrDefault(t => t.Color == null); } @@ -303,7 +319,7 @@ namespace FluentTerminal.App.ViewModels } public bool IsUnderlined => (IsSelected && ApplicationSettings.UnderlineSelectedTab) || - (!IsSelected && ApplicationSettings.InactiveTabColorMode == InactiveTabColorMode.Underlined && TabTheme.Color != null); + (!IsSelected && ApplicationSettings.InactiveTabColorMode == InactiveTabColorMode.Underlined && TabTheme.Theme.Color != null); public bool HasNewOutput { @@ -347,8 +363,6 @@ namespace FluentTerminal.App.ViewModels set => Set(ref _searchWithRegex, value); } - public RelayCommand SelectTabThemeCommand { get; } - public ISettingsService SettingsService { get; } public ShellProfile ShellProfile { get; private set; } @@ -359,7 +373,7 @@ namespace FluentTerminal.App.ViewModels set => Set(ref _showSearchPanel, value); } - public TabTheme TabTheme + public TabThemeViewModel TabTheme { get => _tabTheme; set @@ -370,7 +384,7 @@ namespace FluentTerminal.App.ViewModels } } - public ObservableCollection TabThemes { get; } + public ObservableCollection TabThemes { get; } public Terminal Terminal { get; private set; } @@ -562,12 +576,21 @@ namespace FluentTerminal.App.ViewModels private void OnKeyBindingsChanged(KeyBindingsChangedMessage message) { - ContextMenu = BuidContextMenu(); - } + ApplicationView.ExecuteOnUiThreadAsync(() => + { + var contextMenu = BuidContextMenu(); + if (!contextMenu.EquivalentTo(ContextMenu)) + { + ContextMenu = contextMenu; + } - private void SelectTabTheme(string id) - { - TabTheme = TabThemes.FirstOrDefault(t => t.Id == int.Parse(id)); + var tabContextMenu = BuildTabContextMenu(); + if (!tabContextMenu.EquivalentTo(TabContextMenu)) + { + TabContextMenu = tabContextMenu; + } + + }, CoreDispatcherPriority.Low, true); } private void Terminal_Exited(object sender, int exitCode) @@ -694,55 +717,104 @@ namespace FluentTerminal.App.ViewModels private MenuViewModel BuidContextMenu() { var commandKeyBindings = SettingsService.GetCommandKeyBindings(); - - void addKeyBindings(MenuItemViewModel menuItem, Command command) - { - var keyBindings = commandKeyBindings[command.ToString()]; - if (keyBindings?.Any() == true) - { - menuItem.KeyBinding = new MenuItemKeyBindingViewModel(keyBindings.First()); - } - } - var contextMenu = new MenuViewModel(); + if (!string.IsNullOrWhiteSpace(HoveredUri)) { - var copyLink = new MenuItemViewModel(I18N.Translate("CopyLink"), CopyLinkCommand, icon: "\uE71B"); + var copyLink = new MenuItemViewModel(I18N.Translate("CopyLink"), CopyLinkCommand, icon: Mdl2Icon.Link()); contextMenu.Items.Add(copyLink); contextMenu.Items.Add(new SeparatorMenuItemViewModel()); } - var copy = new MenuItemViewModel(I18N.Translate("Command.Copy"), CopyCommand, icon: "\uE8C8"); - addKeyBindings(copy, Command.Copy); + var copy = new MenuItemViewModel(I18N.Translate("Command.Copy"), CopyCommand, icon: Mdl2Icon.Copy()); + AddKeyBindings(copy, Command.Copy, commandKeyBindings); contextMenu.Items.Add(copy); - var paste = new MenuItemViewModel(I18N.Translate("Command.Paste"), PasteCommand, icon: "\uE77F"); - addKeyBindings(paste, Command.Paste); + var paste = new MenuItemViewModel(I18N.Translate("Command.Paste"), PasteCommand, icon: Mdl2Icon.Paste()); + AddKeyBindings(paste, Command.Paste, commandKeyBindings); contextMenu.Items.Add(paste); contextMenu.Items.Add(new SeparatorMenuItemViewModel()); - var editTitle = new MenuItemViewModel(I18N.Translate("EditTitle.Text"), EditTitleCommand, icon: "\uE70F"); - addKeyBindings(editTitle, Command.ChangeTabTitle); + var editTitle = new MenuItemViewModel(I18N.Translate("EditTitle.Text"), EditTitleCommand, icon: Mdl2Icon.Edit()); + AddKeyBindings(editTitle, Command.ChangeTabTitle, commandKeyBindings); contextMenu.Items.Add(editTitle); - var search = new MenuItemViewModel(I18N.Translate("Search.Text"), ShowSearchPanelCommand, icon: "\uE721"); - addKeyBindings(search, Command.Search); + var search = new MenuItemViewModel(I18N.Translate("Search.Text"), ShowSearchPanelCommand, icon: Mdl2Icon.Search()); + AddKeyBindings(search, Command.Search, commandKeyBindings); contextMenu.Items.Add(search); contextMenu.Items.Add(new SeparatorMenuItemViewModel()); var duplicate = new MenuItemViewModel(I18N.Translate("Command.DuplicateTab"), DuplicateTabCommand); - addKeyBindings(duplicate, Command.DuplicateTab); + AddKeyBindings(duplicate, Command.DuplicateTab, commandKeyBindings); contextMenu.Items.Add(duplicate); contextMenu.Items.Add(new SeparatorMenuItemViewModel()); - var close = new MenuItemViewModel(I18N.Translate("Close"), CloseCommand, icon: "\uE711"); - addKeyBindings(close, Command.CloseTab); + var close = new MenuItemViewModel(I18N.Translate("Close"), CloseCommand, icon: Mdl2Icon.Cancel()); + AddKeyBindings(close, Command.CloseTab, commandKeyBindings); contextMenu.Items.Add(close); return contextMenu; } + + private MenuViewModel BuildTabContextMenu() + { + var commandKeyBindings = SettingsService.GetCommandKeyBindings(); + var contextMenu = new MenuViewModel(); + + RadioMenuItemViewModel createTabColorItem(TabThemeViewModel tabTheme) + { + //todo localise name + var icon = tabTheme.Theme.Id == 0 ? Mdl2Icon.PaginationDotOutline10() : Mdl2Icon.PaginationDotSolid10(tabTheme.Theme.Color); + return new RadioMenuItemViewModel(I18N.Translate($"TabTheme.{tabTheme.Theme.Name}"), $"{Terminal.Id}_tabTheme", tabTheme, bindingPath: nameof(TabThemeViewModel.IsSelected) , icon: icon); + } + + var tabColor = new ExpandableMenuItemViewModel(I18N.Translate("Color.Text")); + + foreach (var tabTheme in TabThemes) + { + tabColor.SubItems.Add(createTabColorItem(tabTheme)); + } + + contextMenu.Items.Add(tabColor); + + var editTitle = new MenuItemViewModel(I18N.Translate("EditTitle.Text"), EditTitleCommand, icon: Mdl2Icon.Edit()); + AddKeyBindings(editTitle, Command.ChangeTabTitle, commandKeyBindings); + contextMenu.Items.Add(editTitle); + + contextMenu.Items.Add(new SeparatorMenuItemViewModel()); + + var duplicate = new MenuItemViewModel(I18N.Translate("Command.DuplicateTab"), DuplicateTabCommand); + AddKeyBindings(duplicate, Command.DuplicateTab, commandKeyBindings); + contextMenu.Items.Add(duplicate); + + contextMenu.Items.Add(new SeparatorMenuItemViewModel()); + + var closeLeft = new MenuItemViewModel(I18N.Translate("CloseLeft.Text"), CloseLeftTabsCommand); + contextMenu.Items.Add(closeLeft); + + var closeRight = new MenuItemViewModel(I18N.Translate("CloseRight.Text"), CloseRightTabsCommand); + contextMenu.Items.Add(closeRight); + + var closeOther = new MenuItemViewModel(I18N.Translate("CloseOther.Text"), CloseOtherTabsCommand); + contextMenu.Items.Add(closeOther); + + var close = new MenuItemViewModel(I18N.Translate("Close"), CloseCommand, icon: Mdl2Icon.Cancel()); + AddKeyBindings(close, Command.CloseTab, commandKeyBindings); + contextMenu.Items.Add(close); + + return contextMenu; + } + + private void AddKeyBindings(MenuItemViewModel menuItem, Command command, IDictionary> commandKeyBindings) + { + var keyBindings = commandKeyBindings[command.ToString()]; + if (keyBindings?.Any() == true) + { + menuItem.KeyBinding = new MenuItemKeyBindingViewModel(keyBindings.First()); + } + } } } \ No newline at end of file diff --git a/FluentTerminal.App/Converters/IconConverter.cs b/FluentTerminal.App/Converters/IconConverter.cs index 24bf9eb..0084859 100644 --- a/FluentTerminal.App/Converters/IconConverter.cs +++ b/FluentTerminal.App/Converters/IconConverter.cs @@ -1,4 +1,6 @@ -using System; +using FluentTerminal.App.ViewModels.Menu; +using Microsoft.Toolkit.Uwp.Helpers; +using System; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Data; using Windows.UI.Xaml.Media; @@ -10,37 +12,19 @@ namespace FluentTerminal.App.Converters { public object Convert(object value, Type targetType, object parameter, string language) { - if (value == null) + if (value is Mdl2Icon mdl2Icon) { - return null; - } + var fontIcon = new FontIcon { FontFamily = new FontFamily("Segoe MDL2 Assets"), Glyph = mdl2Icon.Glyph }; - 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)) + if(!string.IsNullOrWhiteSpace(mdl2Icon.Color)) { - return new SymbolIcon((Symbol) intSymbol); + fontIcon.Foreground = new SolidColorBrush(mdl2Icon.Color.ToColor()); } - throw new ArgumentException($"Integer value isn't defined in {nameof(Symbol)} enum.", nameof(value)); + return fontIcon; } - 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)); + return null; } public object ConvertBack(object value, Type targetType, object parameter, string language) => diff --git a/FluentTerminal.App/Converters/MenuItemViewModelBaseToMenuFlayoutItemBaseConverter.cs b/FluentTerminal.App/Converters/MenuItemViewModelBaseToMenuFlayoutItemBaseConverter.cs index cb93696..efeab0f 100644 --- a/FluentTerminal.App/Converters/MenuItemViewModelBaseToMenuFlayoutItemBaseConverter.cs +++ b/FluentTerminal.App/Converters/MenuItemViewModelBaseToMenuFlayoutItemBaseConverter.cs @@ -6,6 +6,7 @@ using Windows.UI.Xaml.Data; using Windows.UI.Xaml.Input; using FluentTerminal.App.ViewModels.Menu; using FluentTerminal.App.Views; +using Microsoft.UI.Xaml.Controls; // ReSharper disable LocalizableElement @@ -34,6 +35,16 @@ namespace FluentTerminal.App.Converters return GetRegularItem(regular); } + if (viewModel is ToggleMenuItemViewModel toggle) + { + return GetToggleItem(toggle); + } + + if (viewModel is RadioMenuItemViewModel radio) + { + return GetRadioItem(radio); + } + // Won't happen ever, but still... throw new NotImplementedException( $"Unexpected {nameof(MenuItemViewModelBase)} type: {viewModel.GetType()}"); @@ -126,6 +137,85 @@ namespace FluentTerminal.App.Converters return item; } + private static ToggleMenuFlyoutItem GetToggleItem(ToggleMenuItemViewModel viewModel) + { + var item = new ToggleMenuFlyoutItem(); + + 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(ToggleMenuFlyoutItem.IsCheckedProperty, new Binding + { + Source = viewModel.BindingSource, + Path = new PropertyPath(viewModel.BindingPath), + Mode = BindingMode.TwoWay + }); + + return item; + } + + private static RadioMenuFlyoutItem GetRadioItem(RadioMenuItemViewModel viewModel) + { + var item = new RadioMenuFlyoutItem(); + + 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(RadioMenuFlyoutItem.GroupNameProperty, new Binding + { + Source = viewModel, + Path = new PropertyPath(nameof(RadioMenuItemViewModel.GroupName)), + Mode = BindingMode.OneWay + }); + + item.SetBinding(RadioMenuFlyoutItem.IsCheckedProperty, new Binding + { + Source = viewModel.BindingSource, + Path = new PropertyPath(viewModel.BindingPath), + Mode = BindingMode.TwoWay + }); + + return item; + } + #endregion Static #region IValueConverter diff --git a/FluentTerminal.App/Converters/TabColorFallbackConverter.cs b/FluentTerminal.App/Converters/TabColorFallbackConverter.cs index 04f6c06..0c4b740 100644 --- a/FluentTerminal.App/Converters/TabColorFallbackConverter.cs +++ b/FluentTerminal.App/Converters/TabColorFallbackConverter.cs @@ -1,4 +1,4 @@ -using FluentTerminal.Models; +using FluentTerminal.App.ViewModels; using FluentTerminal.Models.Enums; using Microsoft.Toolkit.Uwp.Helpers; using System; @@ -12,8 +12,9 @@ namespace FluentTerminal.App.Converters { public object Convert(object value, Type targetType, object parameter, string language) { - if (value is TabTheme theme && Enum.TryParse((string)parameter, true, out TabThemeKey tabThemeKey)) + if (value is TabThemeViewModel viewModel && Enum.TryParse((string)parameter, true, out TabThemeKey tabThemeKey)) { + var theme = viewModel.Theme; switch (tabThemeKey) { case TabThemeKey.Background: diff --git a/FluentTerminal.App/Converters/TabThemeSelectedConverter.cs b/FluentTerminal.App/Converters/TabThemeSelectedConverter.cs index 70144de..7e3184b 100644 --- a/FluentTerminal.App/Converters/TabThemeSelectedConverter.cs +++ b/FluentTerminal.App/Converters/TabThemeSelectedConverter.cs @@ -1,4 +1,5 @@ -using FluentTerminal.Models; +using FluentTerminal.App.ViewModels; +using FluentTerminal.Models; using System; using Windows.UI.Xaml.Data; @@ -8,9 +9,9 @@ namespace FluentTerminal.App.Converters { public object Convert(object value, Type targetType, object parameter, string language) { - if (value is TabTheme theme && parameter is string idString && int.TryParse(idString, out var id)) + if (value is TerminalViewModel terminal && parameter is string idString && int.TryParse(idString, out var id)) { - return theme.Id == id; + return terminal.TabTheme.Theme.Id == id; } return null; } diff --git a/FluentTerminal.App/Views/TabBar.xaml b/FluentTerminal.App/Views/TabBar.xaml index f3f4eaa..fa2c8de 100644 --- a/FluentTerminal.App/Views/TabBar.xaml +++ b/FluentTerminal.App/Views/TabBar.xaml @@ -2,10 +2,10 @@ x:Class="FluentTerminal.App.Views.TabBar" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:interactivity="using:Microsoft.Xaml.Interactivity" xmlns:behaviors="using:FluentTerminal.App.Behaviors" xmlns:core="using:Microsoft.Xaml.Interactions.Core" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:interactivity="using:Microsoft.Xaml.Interactivity" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:utilities="using:FluentTerminal.App.Utilities" xmlns:viewmodels="using:FluentTerminal.App.ViewModels" @@ -28,9 +28,9 @@ Padding="0,0,0,0" Click="OnScrollLeftButtonTapped" Style="{StaticResource TitleBarRepeatButtonStyle}" + Tapped="OnScrollLeftButtonTapped" ToolTipService.ToolTip="Scroll left" - Visibility="Collapsed" - Tapped="OnScrollLeftButtonTapped"> + Visibility="Collapsed"> @@ -68,6 +68,7 @@ Height="32" Margin="0" Background="Transparent" + ContextFlyout="{x:Bind TabContextMenu, Converter={StaticResource MenuViewModelToFlyoutMenuConverter}, Mode=OneWay}" ToolTipService.ToolTip="{x:Bind TabTitle, Mode=OneWay}"> @@ -78,108 +79,10 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Visibility="Collapsed">