diff --git a/src/App.axaml.cs b/src/App.axaml.cs index e99f88b6..927d1d57 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -112,20 +112,14 @@ namespace SourceGit public static void RaiseException(string context, string message) { - if (Current is App app && app._notificationReceiver != null) - { - var notice = new Models.Notification() { IsError = true, Message = message }; - app._notificationReceiver.OnReceiveNotification(context, notice); - } + if (Current is App app && app._launcher != null) + app._launcher.DispatchNotification(context, message, true); } public static void SendNotification(string context, string message) { - if (Current is App app && app._notificationReceiver != null) - { - var notice = new Models.Notification() { IsError = false, Message = message }; - app._notificationReceiver.OnReceiveNotification(context, notice); - } + if (Current is App app && app._launcher != null) + app._launcher.DispatchNotification(context, message, false); } public static void SetLocale(string localeKey) @@ -285,9 +279,8 @@ namespace SourceGit BindingPlugins.DataValidators.RemoveAt(0); Native.OS.SetupEnternalTools(); - var launcher = new Views.Launcher(); - _notificationReceiver = launcher; - desktop.MainWindow = launcher; + _launcher = new ViewModels.Launcher(); + desktop.MainWindow = new Views.Launcher() { DataContext = _launcher }; if (ViewModels.Preference.Instance.ShouldCheck4UpdateOnStartup) { @@ -330,8 +323,8 @@ namespace SourceGit return default; } + private ViewModels.Launcher _launcher = null; private ResourceDictionary _activeLocale = null; private ResourceDictionary _colorOverrides = null; - private Models.INotificationReceiver _notificationReceiver = null; } } diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index 60cf93c8..2b1ee629 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -2,6 +2,9 @@ using System.IO; using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; using CommunityToolkit.Mvvm.ComponentModel; @@ -40,7 +43,7 @@ namespace SourceGit.ViewModels var root = new Commands.QueryRepositoryRootPath(path).Result(); if (string.IsNullOrEmpty(root)) { - Pages[0].Notifications.Add(new Models.Notification + Pages[0].Notifications.Add(new Notification { IsError = true, Message = $"Given path: '{path}' is NOT a valid repository!" @@ -124,7 +127,7 @@ namespace SourceGit.ViewModels ActivePage = Pages[prevIdx]; } - public void CloseTab(object param) + public void CloseTab(LauncherPage page) { if (Pages.Count == 1) { @@ -148,7 +151,6 @@ namespace SourceGit.ViewModels return; } - LauncherPage page = param as LauncherPage; if (page == null) page = _activePage; @@ -250,6 +252,108 @@ namespace SourceGit.ViewModels ActivePage = page; } + public void DispatchNotification(string pageId, string message, bool isError) + { + var notification = new Notification() { + IsError = isError, + Message = message, + }; + + foreach (var page in Pages) + { + var id = page.Node.Id.Replace("\\", "/"); + if (id == pageId) + { + page.Notifications.Add(notification); + return; + } + } + + if (_activePage != null) + _activePage.Notifications.Add(notification); + } + + public void DismissNotification(Notification notice) + { + if (notice != null) + ActivePage?.Notifications.Remove(notice); + } + + public ContextMenu CreateContextForPageTab(LauncherPage page) + { + if (page == null) + return null; + + var menu = new ContextMenu(); + var close = new MenuItem(); + close.Header = App.Text("PageTabBar.Tab.Close"); + close.InputGesture = KeyGesture.Parse(OperatingSystem.IsMacOS() ? "⌘+W" : "Ctrl+W"); + close.Click += (o, e) => + { + CloseTab(page); + e.Handled = true; + }; + menu.Items.Add(close); + + var closeOthers = new MenuItem(); + closeOthers.Header = App.Text("PageTabBar.Tab.CloseOther"); + closeOthers.Click += (o, e) => + { + CloseOtherTabs(); + e.Handled = true; + }; + menu.Items.Add(closeOthers); + + var closeRight = new MenuItem(); + closeRight.Header = App.Text("PageTabBar.Tab.CloseRight"); + closeRight.Click += (o, e) => + { + CloseRightTabs(); + e.Handled = true; + }; + menu.Items.Add(closeRight); + + if (page.Node.IsRepository) + { + var bookmark = new MenuItem(); + bookmark.Header = App.Text("PageTabBar.Tab.Bookmark"); + bookmark.Icon = App.CreateMenuIcon("Icons.Bookmark"); + + for (int i = 0; i < Models.Bookmarks.Supported.Count; i++) + { + var icon = App.CreateMenuIcon("Icons.Bookmark"); + icon.Fill = Models.Bookmarks.Brushes[i]; + icon.Stroke = App.Current.FindResource("Brush.FG1") as Brush; + icon.StrokeThickness = i == 0 ? 1.0 : 0; + + var dupIdx = i; + var setter = new MenuItem(); + setter.Header = icon; + setter.Click += (o, e) => + { + page.Node.Bookmark = dupIdx; + e.Handled = true; + }; + bookmark.Items.Add(setter); + } + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(bookmark); + + var copyPath = new MenuItem(); + copyPath.Header = App.Text("PageTabBar.Tab.CopyPath"); + copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); + copyPath.Click += (o, e) => + { + page.CopyPath(); + e.Handled = true; + }; + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(copyPath); + } + + return menu; + } + private void CloseRepositoryInTab(LauncherPage page) { if (page.Data is Repository repo) diff --git a/src/ViewModels/LauncherPage.cs b/src/ViewModels/LauncherPage.cs index 703e5eca..b2043a7b 100644 --- a/src/ViewModels/LauncherPage.cs +++ b/src/ViewModels/LauncherPage.cs @@ -24,11 +24,11 @@ namespace SourceGit.ViewModels set => SetProperty(ref _isTabSplitterVisible, value); } - public AvaloniaList Notifications + public AvaloniaList Notifications { get; set; - } = new AvaloniaList(); + } = new AvaloniaList(); public LauncherPage() { @@ -53,12 +53,6 @@ namespace SourceGit.ViewModels App.CopyText(_node.Id); } - public void DismissNotification(object param) - { - if (param is Models.Notification notice) - Notifications.Remove(notice); - } - private RepositoryNode _node = null; private object _data = null; private bool _isTabSplitterVisible = true; diff --git a/src/Models/Notification.cs b/src/ViewModels/Notification.cs similarity index 61% rename from src/Models/Notification.cs rename to src/ViewModels/Notification.cs index 22cf1e67..e54c3790 100644 --- a/src/Models/Notification.cs +++ b/src/ViewModels/Notification.cs @@ -1,4 +1,4 @@ -namespace SourceGit.Models +namespace SourceGit.ViewModels { public class Notification { @@ -10,9 +10,4 @@ App.CopyText(Message); } } - - public interface INotificationReceiver - { - void OnReceiveNotification(string ctx, Notification notice); - } } diff --git a/src/Views/Launcher.axaml b/src/Views/Launcher.axaml index 44bff211..931a8fc7 100644 --- a/src/Views/Launcher.axaml +++ b/src/Views/Launcher.axaml @@ -10,7 +10,6 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.Launcher" x:DataType="vm:Launcher" - x:Name="me" Icon="/App.ico" Title="SourceGit" MinWidth="1280" MinHeight="720" @@ -18,9 +17,9 @@ Height="{Binding Source={x:Static vm:Preference.Instance}, Path=Layout.LauncherHeight, Mode=TwoWay}" WindowState="{Binding Source={x:Static vm:Preference.Instance}, Path=Layout.LauncherWindowState, Mode=TwoWay}" WindowStartupLocation="CenterScreen"> - + - + @@ -40,7 +39,7 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 12,2,0,2 - 11,2,32,2 - 22 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -298,7 +144,7 @@ - + @@ -330,9 +176,8 @@ + + + + + + + + + + + + + + + + diff --git a/src/Views/LauncherTabBar.axaml.cs b/src/Views/LauncherTabBar.axaml.cs new file mode 100644 index 00000000..078b5460 --- /dev/null +++ b/src/Views/LauncherTabBar.axaml.cs @@ -0,0 +1,148 @@ +using System; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace SourceGit.Views +{ + public partial class LauncherTabBar : UserControl + { + public LauncherTabBar() + { + InitializeComponent(); + } + + private void ScrollTabs(object sender, PointerWheelEventArgs e) + { + if (!e.KeyModifiers.HasFlag(KeyModifiers.Shift)) + { + if (e.Delta.Y < 0) + LauncherTabsScroller.LineRight(); + else if (e.Delta.Y > 0) + LauncherTabsScroller.LineLeft(); + e.Handled = true; + } + } + + private void ScrollTabsLeft(object sender, RoutedEventArgs e) + { + LauncherTabsScroller.LineLeft(); + e.Handled = true; + } + + private void ScrollTabsRight(object sender, RoutedEventArgs e) + { + LauncherTabsScroller.LineRight(); + e.Handled = true; + } + + private void OnTabsLayoutUpdated(object sender, EventArgs e) + { + if (LauncherTabsScroller.Extent.Width > LauncherTabsScroller.Viewport.Width) + { + LeftScrollIndicator.IsVisible = true; + LeftScrollIndicator.IsEnabled = LauncherTabsScroller.Offset.X > 0; + RightScrollIndicator.IsVisible = true; + RightScrollIndicator.IsEnabled = LauncherTabsScroller.Offset.X < LauncherTabsScroller.Extent.Width - LauncherTabsScroller.Viewport.Width; + } + else + { + LeftScrollIndicator.IsVisible = false; + RightScrollIndicator.IsVisible = false; + } + } + + private void SetupDragAndDrop(object sender, RoutedEventArgs e) + { + if (sender is Border border) + { + DragDrop.SetAllowDrop(border, true); + border.AddHandler(DragDrop.DropEvent, DropTab); + } + e.Handled = true; + } + + private void OnPointerPressedTab(object sender, PointerPressedEventArgs e) + { + var border = sender as Border; + var point = e.GetCurrentPoint(border); + if (point.Properties.IsMiddleButtonPressed) + { + var vm = DataContext as ViewModels.Launcher; + vm.CloseTab(border.DataContext as ViewModels.LauncherPage); + e.Handled = true; + return; + } + + _pressedTab = true; + _startDragTab = false; + _pressedTabPosition = e.GetPosition(sender as Border); + } + + private void OnPointerReleasedTab(object sender, PointerReleasedEventArgs e) + { + _pressedTab = false; + _startDragTab = false; + } + + private void OnPointerMovedOverTab(object sender, PointerEventArgs e) + { + if (_pressedTab && !_startDragTab && sender is Border border) + { + var delta = e.GetPosition(border) - _pressedTabPosition; + var sizeSquired = delta.X * delta.X + delta.Y * delta.Y; + if (sizeSquired < 64) + return; + + _startDragTab = true; + + var data = new DataObject(); + data.Set("MovedTab", border.DataContext); + DragDrop.DoDragDrop(e, data, DragDropEffects.Move); + } + e.Handled = true; + } + + private void DropTab(object sender, DragEventArgs e) + { + if (e.Data.Contains("MovedTab") && sender is Border border) + { + var to = border.DataContext as ViewModels.LauncherPage; + var moved = e.Data.Get("MovedTab") as ViewModels.LauncherPage; + if (to != null && moved != null && to != moved && DataContext is ViewModels.Launcher vm) + { + vm.MoveTab(moved, to); + } + } + + _pressedTab = false; + _startDragTab = false; + e.Handled = true; + } + + private void OnTabContextRequested(object sender, ContextRequestedEventArgs e) + { + if (sender is Border border && DataContext is ViewModels.Launcher vm) + { + var menu = vm.CreateContextForPageTab(border.DataContext as ViewModels.LauncherPage); + border.OpenContextMenu(menu); + } + + e.Handled = true; + } + + private void OnCloseTab(object sender, RoutedEventArgs e) + { + if (sender is Button btn && DataContext is ViewModels.Launcher vm) + vm.CloseTab(btn.DataContext as ViewModels.LauncherPage); + + e.Handled = true; + } + + private bool _pressedTab = false; + private Point _pressedTabPosition = new Point(); + private bool _startDragTab = false; + } +}