Ver Fonte

Added entry

Krzysztof Krysiński há 2 anos atrás
pai
commit
ef9d9f8006

+ 2 - 1
src/PixiEditor.Avalonia/PixiEditor.Avalonia/App.axaml.cs

@@ -3,6 +3,7 @@ using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Markup.Xaml;
 using PixiEditor.Avalonia.ViewModels;
 using PixiEditor.Avalonia.Views;
+using PixiEditor.Initialization;
 using PixiEditor.UI.Common.Themes;
 using Splat;
 
@@ -19,7 +20,7 @@ public partial class App : Application
     {
         if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
         {
-            desktop.MainWindow = new MainWindow { DataContext = new MainViewModel() };
+            ClassicDesktopEntry entry = new(desktop);
         }
         else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
         {

+ 50 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Helpers/CommandLineHelpers.cs

@@ -0,0 +1,50 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PixiEditor.Helpers.UI;
+
+public static class CommandLineHelpers
+{
+    public static IEnumerable<string> SplitCommandLine(string commandLine)
+    {
+        bool inQuotes = false;
+
+        return commandLine.Split(
+            c =>
+            {
+                if (c == '\"')
+                    inQuotes = !inQuotes;
+
+                return !inQuotes && c == ' ';
+            })
+            .Select(arg => arg.Trim().TrimMatchingQuotes('\"'))
+            .Where(arg => !string.IsNullOrEmpty(arg));
+    }
+
+    public static IEnumerable<string> Split(
+        this string str,
+        Func<char, bool> controller)
+    {
+        int nextPiece = 0;
+
+        for (int c = 0; c < str.Length; c++)
+        {
+            if (controller(str[c]))
+            {
+                yield return str.Substring(nextPiece, c - nextPiece);
+                nextPiece = c + 1;
+            }
+        }
+
+        yield return str.Substring(nextPiece);
+    }
+
+    public static string TrimMatchingQuotes(this string input, char quote)
+    {
+        if ((input.Length >= 2) &&
+            (input[0] == quote) && (input[^1] == quote))
+            return input.Substring(1, input.Length - 2);
+
+        return input;
+    }
+}

+ 4 - 3
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Helpers/MarkupExtensions/LocalizationExtension.cs

@@ -1,5 +1,6 @@
 using Avalonia.Data;
 using Avalonia.Markup.Xaml;
+using Avalonia.Media;
 
 namespace PixiEditor.Helpers;
 
@@ -10,9 +11,9 @@ public class LocalizationExtension : MarkupExtension
 
     public LocalizationExtension(LocalizationExtensionToProvide toProvide)
     {
-        
+
     }
-    
+
     public override object ProvideValue(IServiceProvider serviceProvider)
     {
         switch (toProvide)
@@ -35,7 +36,7 @@ public class LocalizationExtension : MarkupExtension
         ViewModelMain.Current.LocalizationProvider.OnLanguageChanged += _ => expression.UpdateTarget();
 
         return expression;*/
-        return null;
+        return FlowDirection.LeftToRight;
     }
 }
 

+ 198 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Initialization/ClassicDesktopEntry.cs

@@ -0,0 +1,198 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Threading;
+using PixiEditor.Avalonia.ViewModels;
+using PixiEditor.Avalonia.Views;
+using PixiEditor.Helpers.UI;
+using PixiEditor.Models.AppExtensions;
+using PixiEditor.Models.Controllers;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Platform;
+using PixiEditor.Views.Dialogs;
+
+namespace PixiEditor.Initialization;
+
+internal class ClassicDesktopEntry
+{
+        /// <summary>The event mutex name.</summary>
+    private const string UniqueEventName = "33f1410b-2ad7-412a-a468-34fe0a85747c";
+
+    /// <summary>The unique mutex name.</summary>
+    private const string UniqueMutexName = "ab2afe27-b9ee-4f03-a1e4-c18da16a349c";
+
+    /// <summary>The event wait handle.</summary>
+    private EventWaitHandle _eventWaitHandle;
+
+    private string passedArgsFile = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "PixiEditor", ".passedArgs");
+    private IClassicDesktopStyleApplicationLifetime desktop;
+
+    public ClassicDesktopEntry(IClassicDesktopStyleApplicationLifetime desktop)
+    {
+        this.desktop = desktop;
+        desktop.Startup += Start;
+    }
+
+    /// <summary>The mutex.</summary>
+    private Mutex _mutex;
+
+    private void Start(object? sender, ControlledApplicationLifetimeStartupEventArgs e)
+    {
+        StartupArgs.Args = e.Args.ToList();
+        string arguments = string.Join(' ', e.Args);
+
+        if (ParseArgument("--crash (\"?)([A-z0-9:\\/\\ -_.]+)\\1", arguments, out Group[] groups))
+        {
+            CrashReport report = CrashReport.Parse(groups[2].Value);
+            desktop.MainWindow = new CrashReportDialog(report);
+            desktop.MainWindow.Show();
+            return;
+        }
+
+        Dispatcher dispatcher = Dispatcher.UIThread;
+
+        #if !STEAM
+        if (!HandleNewInstance(dispatcher))
+        {
+            return;
+        }
+        #endif
+
+        InitPlatform();
+
+        ExtensionLoader extensionLoader = new ExtensionLoader();
+        extensionLoader.LoadExtensions();
+
+        desktop.MainWindow = new MainWindow(extensionLoader);
+        desktop.MainWindow.Show();
+    }
+
+    private void InitPlatform()
+    {
+        var platform = GetActivePlatform();
+        IPlatform.RegisterPlatform(platform);
+        platform.PerformHandshake();
+    }
+
+    private IPlatform GetActivePlatform()
+    {
+#if STEAM
+        return new PixiEditor.Platform.Steam.SteamPlatform();
+#elif MSIX || MSIX_DEBUG
+        return new PixiEditor.Platform.MSStore.MicrosoftStorePlatform();
+#else
+        return new PixiEditor.Platform.Standalone.StandalonePlatform();
+#endif
+    }
+
+    private bool HandleNewInstance(Dispatcher? dispatcher)
+    {
+        bool isOwned;
+        _mutex = new Mutex(true, UniqueMutexName, out isOwned);
+        _eventWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, UniqueEventName);
+
+        GC.KeepAlive(_mutex);
+
+        if (dispatcher == null)
+            return true;
+
+        if (isOwned)
+        {
+            var thread = new Thread(
+                () =>
+                {
+                    while (_eventWaitHandle.WaitOne())
+                    {
+                        dispatcher.Invoke(
+                            (Action)(() =>
+                            {
+                                if (desktop.MainWindow is MainWindow mainWindow)
+                                {
+                                    mainWindow.BringIntoView();
+                                    List<string> args = new List<string>();
+                                    if (File.Exists(passedArgsFile))
+                                    {
+                                        args = CommandLineHelpers.SplitCommandLine(File.ReadAllText(passedArgsFile)).ToList();
+                                        File.Delete(passedArgsFile);
+                                    }
+
+                                    StartupArgs.Args = args;
+                                    StartupArgs.Args.Add("--openedInExisting");
+                                    MainViewModel viewModel = (MainViewModel)mainWindow.DataContext;
+                                    viewModel.OnStartupCommand.Execute();
+                                }
+                            }));
+                    }
+                })
+            {
+                // It is important mark it as background otherwise it will prevent app from exiting.
+                IsBackground = true
+            };
+
+            thread.Start();
+            return true;
+        }
+
+        // Notify other instance so it could bring itself to foreground.
+        File.WriteAllText(passedArgsFile, string.Join(' ', WrapSpaces(Environment.GetCommandLineArgs())));
+        _eventWaitHandle.Set();
+
+        // Terminate this instance.
+        desktop.Shutdown();
+        return false;
+    }
+
+    private string?[] WrapSpaces(string[] args)
+    {
+        string?[] wrappedArgs = new string?[args.Length];
+        for (int i = 0; i < args.Length; i++)
+        {
+            string arg = args[i];
+            if (arg.Contains(' '))
+            {
+                wrappedArgs[i] = $"\"{arg}\"";
+            }
+            else
+            {
+                wrappedArgs[i] = arg;
+            }
+        }
+
+        return wrappedArgs;
+    }
+
+    //TODO: Implement this
+    /*protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
+    {
+        base.OnSessionEnding(e);
+
+        var vm = ViewModelMain.Current;
+        if (vm is null)
+            return;
+
+        if (vm.DocumentManagerSubViewModel.Documents.Any(x => !x.AllChangesSaved))
+        {
+            ConfirmationType confirmation = ConfirmationDialog.Show(
+                new LocalizedString("SESSION_UNSAVED_DATA", e.ReasonSessionEnding),
+                $"{e.ReasonSessionEnding}");
+            e.Cancel = confirmation != ConfirmationType.Yes;
+        }
+    }*/
+
+    private bool ParseArgument(string pattern, string args, out Group[] groups)
+    {
+        Match match = Regex.Match(args, pattern, RegexOptions.IgnoreCase);
+        groups = null;
+
+        if (match.Success)
+        {
+            groups = match.Groups.Values.ToArray();
+        }
+
+        return match.Success;
+    }
+}

+ 1 - 1
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Commands/Search/ColorSearchResult.cs

@@ -70,7 +70,7 @@ internal class ColorSearchResult : SearchResult
         drawing.Geometry = geometry;
         return new DrawingImage(drawing);
     }
-    
+
     private static TextDecorationCollection GetDecoration(double strokeThickness, SolidColorBrush solidColorBrush) => new()
     {
         new TextDecoration()

+ 11 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Controllers/StartupArgs.cs

@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+
+namespace PixiEditor.Models.Controllers;
+
+/// <summary>
+///     A class that holds startup command line arguments + custom passed ones.
+/// </summary>
+internal static class StartupArgs
+{
+    public static List<string> Args { get; set; }
+}

+ 0 - 4
src/PixiEditor.Avalonia/PixiEditor.Avalonia/PixiEditor.Avalonia.csproj

@@ -48,8 +48,4 @@
     <ItemGroup>
       <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" />
     </ItemGroup>
-  
-    <ItemGroup>
-
-    </ItemGroup>
 </Project>

+ 84 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/CrashReportViewModel.cs

@@ -0,0 +1,84 @@
+using System.Diagnostics;
+using System.IO;
+using System.Net.Http;
+using System.Reactive;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Text;
+using System.Windows;
+using System.Windows.Media;
+using Avalonia;
+using Avalonia.Controls;
+using PixiEditor.Avalonia.ViewModels;
+using PixiEditor.Avalonia.Views;
+using PixiEditor.Helpers;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Views;
+using PixiEditor.Views.Dialogs;
+using ReactiveUI;
+
+namespace PixiEditor.ViewModels;
+
+internal class CrashReportViewModel : ViewModelBase
+{
+    private bool hasRecoveredDocuments = true;
+
+    public CrashReport CrashReport { get; }
+
+    public string ReportText { get; }
+
+    public int DocumentCount { get; }
+
+    public ReactiveCommand<Unit, Unit> OpenSendCrashReportCommand { get; }
+
+    public ReactiveCommand<Unit, Unit> RecoverDocumentsCommand { get; }
+
+    public ReactiveCommand<Unit, Unit> AttachDebuggerCommand { get; }
+
+    public bool IsDebugBuild { get; set; }
+
+    public CrashReportViewModel(CrashReport report)
+    {
+        SetIsDebug();
+
+        CrashReport = report;
+        ReportText = report.ReportText;
+        DocumentCount = report.GetDocumentCount();
+        //TODO: Implement
+        //OpenSendCrashReportCommand = ReactiveCommand.Create(() => new SendCrashReportWindow(CrashReport).Show());
+        RecoverDocumentsCommand = ReactiveCommand.Create(RecoverDocuments, Observable.Create((IObserver<bool> observer) =>
+        {
+            observer.OnNext(hasRecoveredDocuments);
+            observer.OnCompleted();
+            return Disposable.Empty;
+        }));
+
+        AttachDebuggerCommand = ReactiveCommand.Create(AttachDebugger);
+
+        if (!IsDebugBuild)
+            _ = CrashHelper.SendReportTextToWebhook(report);
+    }
+
+    public void RecoverDocuments()
+    {
+        MainWindow window = MainWindow.CreateWithDocuments(CrashReport.RecoverDocuments());
+
+        Application.Current.Run(window);
+        window.Show();
+        hasRecoveredDocuments = false;
+    }
+
+    [Conditional("DEBUG")]
+    private void SetIsDebug()
+    {
+        IsDebugBuild = true;
+    }
+
+    private void AttachDebugger()
+    {
+        if (!Debugger.Launch())
+        {
+            /*TODO: MessageBox.Show("Starting debugger failed", "Starting debugger failed", MessageBoxButton.OK, MessageBoxImage.Error);*/
+        }
+    }
+}

+ 16 - 1
src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/MainViewModel.cs

@@ -1,5 +1,20 @@
-namespace PixiEditor.Avalonia.ViewModels;
+using System.Reactive;
+using ReactiveUI;
+
+namespace PixiEditor.Avalonia.ViewModels;
 
 public class MainViewModel : ViewModelBase
 {
+    public event Action OnStartupEvent;
+    public ReactiveCommand<Unit, Unit> OnStartupCommand { get; }
+
+    public MainViewModel()
+    {
+        OnStartupCommand = ReactiveCommand.Create(OnStartup);
+    }
+
+    private void OnStartup()
+    {
+        OnStartupEvent?.Invoke();
+    }
 }

+ 54 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/Dialogs/CrashReportDialog.axaml

@@ -0,0 +1,54 @@
+<Window x:Class="PixiEditor.Views.Dialogs.CrashReportDialog"
+        x:ClassModifier="internal"
+        xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:vm="clr-namespace:PixiEditor.Avalonia.ViewModels"
+        xmlns:main="clr-namespace:PixiEditor.Avalonia.Views.Main"
+        xmlns:viewModels="clr-namespace:PixiEditor.ViewModels"
+        mc:Ignorable="d"
+        Background="{StaticResource ThemeBackgroundBrush1}" Foreground="White"
+        Title="PixiEditor has crashed!"
+        MinWidth="480" MinHeight="195"
+        WindowStartupLocation="CenterScreen"
+        Width="480" Height="195">
+
+    <!--<WindowChrome.WindowChrome>
+        <WindowChrome CaptionHeight="32" GlassFrameThickness="0.1"
+                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
+    </WindowChrome.WindowChrome>
+
+    <Window.CommandBindings>
+        <CommandBinding Command="{x:Static SystemCommands.CloseWindowCommand}" CanExecute="CommandBinding_CanExecute"
+                        Executed="CommandBinding_Executed_Close" />
+    </Window.CommandBindings>-->
+
+    <Grid>
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto"/>
+            <RowDefinition/>
+        </Grid.RowDefinitions>
+        <!--<DialogTitleBar TitleKey="PixiEditor has crashed!" CloseCommand="{x:Static SystemCommands.CloseWindowCommand}" />-->
+        <Grid Grid.Row="1" Margin="30,30,30,0" >
+            <StackPanel>
+                <Grid Background="{StaticResource ThemeBackgroundBrush}">
+                    <StackPanel Margin="7" VerticalAlignment="Center">
+                        <TextBlock Text="{Binding DocumentCount, StringFormat={}{0} file(s) might be recoverable}"
+                       d:Text="2 file(s) can be recovered"/>
+                        <TextBlock TextWrapping="Wrap">You can help the developers fix this bug by sending a crash report that was generated (you will still be able to recover the files).</TextBlock>
+                    </StackPanel>
+                </Grid>
+
+                <WrapPanel Margin="0,20,0,5" Orientation="Horizontal" HorizontalAlignment="Center">
+                    <Button Command="{Binding OpenSendCrashReportCommand}"
+                        Width="120">Send report</Button>
+                    <Button Margin="5,0,5,0" Width="120"
+                        Command="{Binding RecoverDocumentsCommand}">Recover files</Button>
+                    <Button IsVisible="{Binding IsDebugBuild}" Width="170"
+                    Command="{Binding AttachDebuggerCommand}">(Re)Attach debugger</Button>
+                </WrapPanel>
+            </StackPanel>
+        </Grid>
+    </Grid>
+</Window>

+ 26 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/Dialogs/CrashReportDialog.axaml.cs

@@ -0,0 +1,26 @@
+using Avalonia.Controls;
+using PixiEditor.Models.DataHolders;
+
+namespace PixiEditor.Views.Dialogs;
+
+/// <summary>
+/// Interaction logic for CrashReportDialog.xaml
+/// </summary>
+internal partial class CrashReportDialog : Window
+{
+    public CrashReportDialog(CrashReport report)
+    {
+        DataContext = new CrashReportViewModel(report);
+        InitializeComponent();
+    }
+
+    /*private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
+    {
+        e.CanExecute = true;
+    }
+
+    private void CommandBinding_Executed_Close(object sender, ExecutedRoutedEventArgs e)
+    {
+        SystemCommands.CloseWindow(this);
+    }*/
+}

+ 2 - 2
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/Dialogs/NoticePopup.axaml

@@ -11,7 +11,7 @@
         d:Title="Notice" Height="180" Width="400" MinHeight="180" MinWidth="400"
         WindowStartupLocation="CenterScreen"
         x:Name="popup"
-        ui:Translator.Key="{Binding ElementName=popup, Path=Title}"
+        ui:Translator.Key="{Binding ElementName=popup, Path=Title, Mode=OneTime}"
         FlowDirection="{helpers:Localization FlowDirection}">
 
     <!--<WindowChrome.WindowChrome>
@@ -19,7 +19,7 @@
                       ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
     </WindowChrome.WindowChrome>-->
 
-    <DockPanel Background="{StaticResource AccentColor}" Focusable="True">
+    <DockPanel Background="{StaticResource ThemeBackgroundBrush1}" Focusable="True">
         <Interaction.Behaviors>
             <!--<behaviours:ClearFocusOnClickBehavior/>-->
         </Interaction.Behaviors>

+ 0 - 6
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/Dialogs/NoticePopup.axaml.cs

@@ -12,12 +12,6 @@ internal partial class NoticePopup : Window
     public static readonly StyledProperty<string> BodyProperty =
         AvaloniaProperty.Register<NoticePopup, string>(nameof(Body));
 
-    public new string Title
-    {
-        get => base.Title;
-        set => base.Title = value;
-    }
-
     public string Body
     {
         get => (string)GetValue(BodyProperty);

+ 1 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/MainWindow.axaml

@@ -8,6 +8,7 @@
         Width="1600"
         mc:Ignorable="d" d:DesignWidth="1600" d:DesignHeight="1000"
         x:Class="PixiEditor.Avalonia.Views.MainWindow"
+        x:ClassModifier="internal"
         WindowStartupLocation="CenterScreen"
         WindowState="Maximized"
         Icon="/Assets/avalonia-logo.ico"

+ 20 - 5
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/MainWindow.axaml.cs

@@ -1,18 +1,33 @@
-using Avalonia;
+using System.Collections.Generic;
 using Avalonia.Controls;
-using Avalonia.Platform;
+using PixiEditor.Models.AppExtensions;
 using PixiEditor.Models.Localization;
-using Splat;
 
 namespace PixiEditor.Avalonia.Views;
 
-public partial class MainWindow : Window
+internal partial class MainWindow : Window
 {
-    public MainWindow()
+    public MainWindow(ExtensionLoader extensionLoader)
     {
         LocalizationProvider localizationProvider = new LocalizationProvider(null);
         localizationProvider.LoadData(/*TODO: IPreferences.Current.GetPreference<string>("LanguageCode");*/);
 
         InitializeComponent();
     }
+
+    public static MainWindow CreateWithDocuments(IEnumerable<(string? originalPath, byte[] dotPixiBytes)> documents)
+    {
+        //TODO: Implement this
+        /*MainWindow window = new(extLoader);
+        FileViewModel fileVM = window.services.GetRequiredService<FileViewModel>();
+
+        foreach (var (path, bytes) in documents)
+        {
+            fileVM.OpenRecoveredDotPixi(path, bytes);
+        }
+
+        return window;*/
+
+        return new MainWindow(null);
+    }
 }