diff --git a/AVALONIA_MIGRATION.md b/AVALONIA_MIGRATION.md new file mode 100644 index 000000000..afdaa6bd3 --- /dev/null +++ b/AVALONIA_MIGRATION.md @@ -0,0 +1,166 @@ +# Avalonia UI Migration Documentation + +## Overview + +This document describes the WPF to Avalonia UI migration for the Algoloop application. The migration creates a cross-platform UI using Avalonia UI 11.2.2 targeting .NET 8.0. + +## Project Structure + +### New Project: Algoloop.Avalonia + +Located in: `Algoloop.Avalonia/` + +**Key Files:** +- `Program.cs` - Application entry point +- `App.axaml` / `App.axaml.cs` - Application configuration with Fluent theme +- `Views/MainWindow.axaml` / `Views/MainWindow.axaml.cs` - Main application window +- `ViewModels/` - Stub ViewModels for initial implementation +- `Model/` - Business logic models +- `Resources/` - UI resources (icons, images) + +## How to Run + +### Prerequisites +- .NET 8.0 SDK or later +- For Windows: No additional requirements +- For Linux: X11 display server +- For macOS: XQuartz (X11 for macOS) + +### Build and Run + +```bash +# Navigate to repository root +cd /path/to/Algoloop + +# Restore dependencies +dotnet restore Algoloop.Avalonia/Algoloop.Avalonia.csproj + +# Build the project +dotnet build Algoloop.Avalonia/Algoloop.Avalonia.csproj + +# Run the application +dotnet run --project Algoloop.Avalonia/Algoloop.Avalonia.csproj +``` + +### Running from Visual Studio +1. Open `Algoloop.sln` in Visual Studio 2022 or later +2. Set `Algoloop.Avalonia` as the startup project +3. Press F5 to run + +## Current Implementation Status + +### ✅ Completed +- [x] Created Algoloop.Avalonia project targeting .NET 8.0 +- [x] Added Avalonia NuGet packages (11.2.2) +- [x] Implemented App.axaml with Fluent theme +- [x] Created MainWindow with basic UI layout +- [x] Added Program.cs entry point +- [x] Integrated ViewModelLocator and dependency injection +- [x] Configured logging infrastructure +- [x] Added resources (icons, images) +- [x] Application builds successfully +- [x] Application starts (requires display server) + +### ⚠️ Known Limitations + +1. **ViewModels are Stubs**: Current ViewModels are minimal implementations. Full business logic from WPF ViewModels needs to be migrated. + +2. **View Implementations**: Tab content views (Markets, Strategies, Research, Logs) show placeholder text. These need to be implemented with proper Avalonia controls. + +3. **WPF-Specific Dependencies**: The following WPF-specific features need Avalonia equivalents: + - DevExpress ThemedWindow → Replaced with standard Avalonia Window + - WebView2 (for Research tab) → Needs cross-platform alternative + - Extended WPF Toolkit controls → Need Avalonia replacements + - OxyPlot.Wpf → Should use OxyPlot.Avalonia + - StockSharp charting → Needs evaluation for Avalonia compatibility + +4. **Missing Features**: + - Settings dialog + - About dialog + - Theme switching + - Detailed tab implementations + - Data binding to real business logic + - Chart implementations + +5. **Platform-Specific Considerations**: + - Window placement/state persistence uses WPF APIs - needs Avalonia solution + - Some keyboard shortcuts may behave differently + - File dialogs use platform-native implementations + +## Migration Notes + +### Replaced Components + +| WPF Component | Avalonia Replacement | Status | +|---------------|---------------------|--------| +| dx:ThemedWindow | Window | ✅ Complete | +| System.Windows.Controls.Menu | Avalonia.Controls.Menu | ✅ Complete | +| System.Windows.Controls.TabControl | Avalonia.Controls.TabControl | ✅ Complete | +| StatusBar | Border + TextBlock | ✅ Complete | +| WebView2 | Placeholder TextBlock | ⚠️ Needs replacement | + +### Code Changes Made + +1. **Removed WPF Dependencies**: + - Changed from `UseWPF` to Avalonia SDK + - Removed Windows-specific targeting + - Updated to .NET 8.0 + +2. **XAML Migration**: + - Changed namespace from `http://schemas.microsoft.com/winfx/2006/xaml/presentation` to `https://github.com/avaloniaui` + - Updated property names (e.g., `DockPanel.Dock` works the same) + - Disabled compiled bindings for dynamic resource access + +3. **Code-Behind Changes**: + - Changed `System.Windows.Window` to `Avalonia.Controls.Window` + - Updated event handler signatures + - Cross-platform URL opening logic + +## Next Steps + +To complete the migration: + +1. **Implement Full ViewModels**: + - Port business logic from WPF ViewModels + - Remove WPF-specific dependencies (Window, DataGridColumn, etc.) + - Implement proper commands and data binding + +2. **Implement View Content**: + - Create MarketsView.axaml + - Create StrategiesView.axaml + - Create LogView.axaml + - Replace WebView2 in Research tab + +3. **Add Dialogs**: + - Settings dialog + - About dialog + - Other modal dialogs + +4. **Charting**: + - Integrate OxyPlot.Avalonia + - Port chart implementations + +5. **Testing**: + - Test on Windows, Linux, and macOS + - Verify data binding and commands + - Performance testing + +## References + +- [Avalonia UI Documentation](https://docs.avaloniaui.net/) +- [Avalonia Samples](https://github.com/AvaloniaUI/Avalonia.Samples) +- [WPF to Avalonia Migration Guide](https://docs.avaloniaui.net/docs/guides/platforms/wpf-migration) +- [Avalonia Community](https://github.com/AvaloniaUI/Avalonia/discussions) + +## Support + +For issues related to the Avalonia migration, please: +1. Check existing GitHub issues +2. Review Avalonia documentation +3. Create a new issue with details about the problem + +--- + +**Last Updated**: October 2025 +**Avalonia Version**: 11.2.2 +**Target Framework**: .NET 8.0 diff --git a/Algoloop.Avalonia/Algoloop.Avalonia.csproj b/Algoloop.Avalonia/Algoloop.Avalonia.csproj new file mode 100644 index 000000000..a64a1cad6 --- /dev/null +++ b/Algoloop.Avalonia/Algoloop.Avalonia.csproj @@ -0,0 +1,41 @@ + + + WinExe + net8.0 + x64 + x64 + enable + true + app.manifest + Resources\AlgoloopIcon.ico + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Algoloop.Avalonia/App.axaml b/Algoloop.Avalonia/App.axaml new file mode 100644 index 000000000..33fadb4a5 --- /dev/null +++ b/Algoloop.Avalonia/App.axaml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/Algoloop.Avalonia/App.axaml.cs b/Algoloop.Avalonia/App.axaml.cs new file mode 100644 index 000000000..2f6dc94f7 --- /dev/null +++ b/Algoloop.Avalonia/App.axaml.cs @@ -0,0 +1,124 @@ +/* + * Copyright 2018 Capnode AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Algoloop.Avalonia.Views; +using Algoloop.Wpf.Model; +using Algoloop.Wpf.ViewModels; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using QuantConnect.Configuration; +using QuantConnect.Logging; +using System; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; + +namespace Algoloop.Avalonia +{ + public partial class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + + // Set working directory to exe directory + string unc = Assembly.GetExecutingAssembly().Location; + string folder = Path.GetDirectoryName(unc) ?? string.Empty; + Directory.SetCurrentDirectory(folder); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + // Set Log handler + string logfile = Path.Combine(MainService.GetProgramDataFolder(), AboutModel.Product + ".log"); + File.Delete(logfile); + Log.DebuggingEnabled = Config.GetBool("debug-mode", false); + Log.DebuggingLevel = Config.GetInt("debug-level", 1); + Log.LogHandler = new LogItemHandler(logfile); + + // Exception Handling Wiring + AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionHandler; + TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionHandler; + + desktop.MainWindow = new MainWindow(); + + desktop.ShutdownRequested += OnShutdownRequested; + } + + base.OnFrameworkInitializationCompleted(); + } + + private void OnShutdownRequested(object? sender, ShutdownRequestedEventArgs e) + { + ViewModelLocator.ResearchViewModel?.StopJupyter(); + ViewModelLocator.MainViewModel?.SaveConfig(); + Log.Trace($"Exit \"{AboutModel.Product}\""); + } + + public static void LogError(Exception ex, string? message = null) + { + if (string.IsNullOrEmpty(message)) + { + Log.Error($"{ex.GetType()}: {ex.Message}", true); + } + else + { + Log.Error($"{message} {ex.GetType()}: {ex.Message}", true); + } + + if (ex.InnerException != null) + { + LogError(ex.InnerException, message); + } + + if (ex is ReflectionTypeLoadException rex) + { + foreach (Exception exception in rex.LoaderExceptions ?? Array.Empty()) + { + LogError(exception, message); + } + } + } + + private void UnobservedTaskExceptionHandler(object? sender, UnobservedTaskExceptionEventArgs e) + { + e?.SetObserved(); // Prevents the Program from terminating. + + if (e.Exception != null && e.Exception is Exception tuex) + { + LogError(tuex, nameof(UnobservedTaskExceptionHandler)); + } + else if (sender is Exception ex) + { + LogError(ex, nameof(UnobservedTaskExceptionHandler)); + } + } + + private void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e) + { + if (e.ExceptionObject != null && e.ExceptionObject is Exception uex) + { + LogError(uex, nameof(UnhandledExceptionHandler)); + } + else if (sender is Exception ex) + { + Log.Error(ex, nameof(UnhandledExceptionHandler)); + } + } + } +} diff --git a/Algoloop.Avalonia/Model/AboutModel.cs b/Algoloop.Avalonia/Model/AboutModel.cs new file mode 100644 index 000000000..1a345d560 --- /dev/null +++ b/Algoloop.Avalonia/Model/AboutModel.cs @@ -0,0 +1,9 @@ +namespace Algoloop.Wpf.Model +{ + public static class AboutModel + { + public static string Product => "Algoloop"; + public static string Title => "Algoloop"; + public static string Version => "1.0.0-avalonia"; + } +} diff --git a/Algoloop.Avalonia/Model/MainService.cs b/Algoloop.Avalonia/Model/MainService.cs new file mode 100644 index 000000000..af768d283 --- /dev/null +++ b/Algoloop.Avalonia/Model/MainService.cs @@ -0,0 +1,29 @@ +using System; +using System.IO; +using System.Reflection; + +namespace Algoloop.Wpf.Model +{ + public static class MainService + { + public static string GetProgramFolder() + { + string unc = Assembly.GetExecutingAssembly().Location; + return Path.GetDirectoryName(unc) ?? string.Empty; + } + + public static string GetProgramDataFolder() + { + string programDataFolder = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + AboutModel.Product); + + if (!Directory.Exists(programDataFolder)) + { + Directory.CreateDirectory(programDataFolder); + } + + return programDataFolder; + } + } +} diff --git a/Algoloop.Avalonia/Program.cs b/Algoloop.Avalonia/Program.cs new file mode 100644 index 000000000..88e871362 --- /dev/null +++ b/Algoloop.Avalonia/Program.cs @@ -0,0 +1,31 @@ +/* + * Copyright 2018 Capnode AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Avalonia; +using System; + +namespace Algoloop.Avalonia +{ + internal sealed class Program + { + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); + } +} diff --git a/Algoloop.Avalonia/Resources/Add.png b/Algoloop.Avalonia/Resources/Add.png new file mode 100644 index 000000000..1b2183677 Binary files /dev/null and b/Algoloop.Avalonia/Resources/Add.png differ diff --git a/Algoloop.Avalonia/Resources/AddList.png b/Algoloop.Avalonia/Resources/AddList.png new file mode 100644 index 000000000..4cfb8987e Binary files /dev/null and b/Algoloop.Avalonia/Resources/AddList.png differ diff --git a/Algoloop.Avalonia/Resources/AddText.png b/Algoloop.Avalonia/Resources/AddText.png new file mode 100644 index 000000000..7c471a604 Binary files /dev/null and b/Algoloop.Avalonia/Resources/AddText.png differ diff --git a/Algoloop.Avalonia/Resources/Algoloop.png b/Algoloop.Avalonia/Resources/Algoloop.png new file mode 100644 index 000000000..5941adae3 Binary files /dev/null and b/Algoloop.Avalonia/Resources/Algoloop.png differ diff --git a/Algoloop.Avalonia/Resources/Algoloop1080.png b/Algoloop.Avalonia/Resources/Algoloop1080.png new file mode 100644 index 000000000..070afb929 Binary files /dev/null and b/Algoloop.Avalonia/Resources/Algoloop1080.png differ diff --git a/Algoloop.Avalonia/Resources/AlgoloopColor.png b/Algoloop.Avalonia/Resources/AlgoloopColor.png new file mode 100644 index 000000000..0a0aee591 Binary files /dev/null and b/Algoloop.Avalonia/Resources/AlgoloopColor.png differ diff --git a/Algoloop.Avalonia/Resources/AlgoloopIcon.ico b/Algoloop.Avalonia/Resources/AlgoloopIcon.ico new file mode 100644 index 000000000..c1ca3ca54 Binary files /dev/null and b/Algoloop.Avalonia/Resources/AlgoloopIcon.ico differ diff --git a/Algoloop.Avalonia/Resources/AlgoloopSplash.png b/Algoloop.Avalonia/Resources/AlgoloopSplash.png new file mode 100644 index 000000000..6f072555d Binary files /dev/null and b/Algoloop.Avalonia/Resources/AlgoloopSplash.png differ diff --git a/Algoloop.Avalonia/Resources/Checklist.png b/Algoloop.Avalonia/Resources/Checklist.png new file mode 100644 index 000000000..abc0f6d31 Binary files /dev/null and b/Algoloop.Avalonia/Resources/Checklist.png differ diff --git a/Algoloop.Avalonia/Resources/Clone.png b/Algoloop.Avalonia/Resources/Clone.png new file mode 100644 index 000000000..d6d82fe4b Binary files /dev/null and b/Algoloop.Avalonia/Resources/Clone.png differ diff --git a/Algoloop.Avalonia/Resources/Delete.png b/Algoloop.Avalonia/Resources/Delete.png new file mode 100644 index 000000000..089895322 Binary files /dev/null and b/Algoloop.Avalonia/Resources/Delete.png differ diff --git a/Algoloop.Avalonia/Resources/Down.png b/Algoloop.Avalonia/Resources/Down.png new file mode 100644 index 000000000..817589875 Binary files /dev/null and b/Algoloop.Avalonia/Resources/Down.png differ diff --git a/Algoloop.Avalonia/Resources/Download1.png b/Algoloop.Avalonia/Resources/Download1.png new file mode 100644 index 000000000..c8c67680a Binary files /dev/null and b/Algoloop.Avalonia/Resources/Download1.png differ diff --git a/Algoloop.Avalonia/Resources/Download2.png b/Algoloop.Avalonia/Resources/Download2.png new file mode 100644 index 000000000..b472dae86 Binary files /dev/null and b/Algoloop.Avalonia/Resources/Download2.png differ diff --git a/Algoloop.Avalonia/Resources/Download3.png b/Algoloop.Avalonia/Resources/Download3.png new file mode 100644 index 000000000..1c542b2b6 Binary files /dev/null and b/Algoloop.Avalonia/Resources/Download3.png differ diff --git a/Algoloop.Avalonia/Resources/Exit.png b/Algoloop.Avalonia/Resources/Exit.png new file mode 100644 index 000000000..e5041ed8a Binary files /dev/null and b/Algoloop.Avalonia/Resources/Exit.png differ diff --git a/Algoloop.Avalonia/Resources/Export.png b/Algoloop.Avalonia/Resources/Export.png new file mode 100644 index 000000000..72a9ddb4a Binary files /dev/null and b/Algoloop.Avalonia/Resources/Export.png differ diff --git a/Algoloop.Avalonia/Resources/Help.png b/Algoloop.Avalonia/Resources/Help.png new file mode 100644 index 000000000..2238c8fdb Binary files /dev/null and b/Algoloop.Avalonia/Resources/Help.png differ diff --git a/Algoloop.Avalonia/Resources/Import.png b/Algoloop.Avalonia/Resources/Import.png new file mode 100644 index 000000000..e4703e99d Binary files /dev/null and b/Algoloop.Avalonia/Resources/Import.png differ diff --git a/Algoloop.Avalonia/Resources/Info.png b/Algoloop.Avalonia/Resources/Info.png new file mode 100644 index 000000000..0c3dad288 Binary files /dev/null and b/Algoloop.Avalonia/Resources/Info.png differ diff --git a/Algoloop.Avalonia/Resources/MoveDown.png b/Algoloop.Avalonia/Resources/MoveDown.png new file mode 100644 index 000000000..ea66d5515 Binary files /dev/null and b/Algoloop.Avalonia/Resources/MoveDown.png differ diff --git a/Algoloop.Avalonia/Resources/MoveUp.png b/Algoloop.Avalonia/Resources/MoveUp.png new file mode 100644 index 000000000..557d5e6a9 Binary files /dev/null and b/Algoloop.Avalonia/Resources/MoveUp.png differ diff --git a/Algoloop.Avalonia/Resources/New.png b/Algoloop.Avalonia/Resources/New.png new file mode 100644 index 000000000..2985b8d77 Binary files /dev/null and b/Algoloop.Avalonia/Resources/New.png differ diff --git a/Algoloop.Avalonia/Resources/Palette.png b/Algoloop.Avalonia/Resources/Palette.png new file mode 100644 index 000000000..3d69f179d Binary files /dev/null and b/Algoloop.Avalonia/Resources/Palette.png differ diff --git a/Algoloop.Avalonia/Resources/Refresh.png b/Algoloop.Avalonia/Resources/Refresh.png new file mode 100644 index 000000000..2221aea4b Binary files /dev/null and b/Algoloop.Avalonia/Resources/Refresh.png differ diff --git a/Algoloop.Avalonia/Resources/Run.png b/Algoloop.Avalonia/Resources/Run.png new file mode 100644 index 000000000..105ac78dd Binary files /dev/null and b/Algoloop.Avalonia/Resources/Run.png differ diff --git a/Algoloop.Avalonia/Resources/Save.png b/Algoloop.Avalonia/Resources/Save.png new file mode 100644 index 000000000..2ade0ad04 Binary files /dev/null and b/Algoloop.Avalonia/Resources/Save.png differ diff --git a/Algoloop.Avalonia/Resources/SortTextDown.png b/Algoloop.Avalonia/Resources/SortTextDown.png new file mode 100644 index 000000000..8a5f5b5a1 Binary files /dev/null and b/Algoloop.Avalonia/Resources/SortTextDown.png differ diff --git a/Algoloop.Avalonia/Resources/Stop.png b/Algoloop.Avalonia/Resources/Stop.png new file mode 100644 index 000000000..5580fecce Binary files /dev/null and b/Algoloop.Avalonia/Resources/Stop.png differ diff --git a/Algoloop.Avalonia/Resources/Tools.png b/Algoloop.Avalonia/Resources/Tools.png new file mode 100644 index 000000000..233d3a123 Binary files /dev/null and b/Algoloop.Avalonia/Resources/Tools.png differ diff --git a/Algoloop.Avalonia/Resources/Up.png b/Algoloop.Avalonia/Resources/Up.png new file mode 100644 index 000000000..b034dc9d5 Binary files /dev/null and b/Algoloop.Avalonia/Resources/Up.png differ diff --git a/Algoloop.Avalonia/ViewModels/LogItemHandler.cs b/Algoloop.Avalonia/ViewModels/LogItemHandler.cs new file mode 100644 index 000000000..d6616501d --- /dev/null +++ b/Algoloop.Avalonia/ViewModels/LogItemHandler.cs @@ -0,0 +1,45 @@ +using QuantConnect.Logging; +using System; +using System.IO; + +namespace Algoloop.Wpf.ViewModels +{ + public class LogItemHandler : ILogHandler + { + private readonly string _logFilePath; + private readonly StreamWriter? _writer; + + public LogItemHandler(string logFile) + { + _logFilePath = logFile; + try + { + _writer = new StreamWriter(_logFilePath, append: true) { AutoFlush = true }; + } + catch + { + _writer = null; + } + } + + public void Debug(string text) + { + _writer?.WriteLine($"[DEBUG] {DateTime.Now:yyyy-MM-dd HH:mm:ss} {text}"); + } + + public void Error(string text) + { + _writer?.WriteLine($"[ERROR] {DateTime.Now:yyyy-MM-dd HH:mm:ss} {text}"); + } + + public void Trace(string text) + { + _writer?.WriteLine($"[TRACE] {DateTime.Now:yyyy-MM-dd HH:mm:ss} {text}"); + } + + public void Dispose() + { + _writer?.Dispose(); + } + } +} diff --git a/Algoloop.Avalonia/ViewModels/MainViewModel.cs b/Algoloop.Avalonia/ViewModels/MainViewModel.cs new file mode 100644 index 000000000..6c0d2fc85 --- /dev/null +++ b/Algoloop.Avalonia/ViewModels/MainViewModel.cs @@ -0,0 +1,94 @@ +using Algoloop.Wpf.Model; +using CommunityToolkit.Mvvm.Input; +using System.Collections.ObjectModel; +using System.Windows.Input; + +namespace Algoloop.Wpf.ViewModels +{ + public class MainViewModel : ViewModelBase + { + private bool _isBusy; + private string _statusMessage = string.Empty; + + public MainViewModel( + SettingsViewModel settingsViewModel, + MarketsViewModel marketsViewModel, + StrategiesViewModel strategiesViewModel, + ResearchViewModel researchViewModel, + LogViewModel logViewModel) + { + SettingsViewModel = settingsViewModel; + MarketsViewModel = marketsViewModel; + StrategiesViewModel = strategiesViewModel; + ResearchViewModel = researchViewModel; + LogViewModel = logViewModel; + + SaveCommand = new RelayCommand(() => SaveConfig(), () => !IsBusy); + ExitCommand = new RelayCommand(_ => DoExit(), _ => !IsBusy); + } + + public ICommand SaveCommand { get; } + public ICommand ExitCommand { get; } + public SettingsViewModel SettingsViewModel { get; } + public MarketsViewModel MarketsViewModel { get; } + public StrategiesViewModel StrategiesViewModel { get; } + public ResearchViewModel ResearchViewModel { get; } + public LogViewModel LogViewModel { get; } + + public static string Title => $"{AboutModel.Title} {AboutModel.Version}"; + + public bool IsBusy + { + get => _isBusy; + set => SetProperty(ref _isBusy, value); + } + + public string StatusMessage + { + get => _statusMessage; + set => SetProperty(ref _statusMessage, value); + } + + public void SaveConfig() + { + StatusMessage = "Configuration saved"; + } + + public void DoSettings(bool update) + { + StatusMessage = "Settings updated"; + } + + private void DoExit() + { + System.Environment.Exit(0); + } + } + + public class SettingsViewModel : ViewModelBase { } + + public class MarketsViewModel : ViewModelBase + { + public ObservableCollection Markets { get; } = new ObservableCollection(); + } + + public class StrategiesViewModel : ViewModelBase + { + public ObservableCollection Strategies { get; } = new ObservableCollection(); + } + + public class ResearchViewModel : ViewModelBase + { + public string Source => "about:blank"; + + public void StopJupyter() + { + // Stub implementation + } + } + + public class LogViewModel : ViewModelBase + { + public ObservableCollection Logs { get; } = new ObservableCollection(); + } +} diff --git a/Algoloop.Avalonia/ViewModels/ViewModelBase.cs b/Algoloop.Avalonia/ViewModels/ViewModelBase.cs new file mode 100644 index 000000000..fa2757c2f --- /dev/null +++ b/Algoloop.Avalonia/ViewModels/ViewModelBase.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace Algoloop.Wpf.ViewModels +{ + public class ViewModelBase : INotifyPropertyChanged + { + public event PropertyChangedEventHandler? PropertyChanged; + + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + protected bool SetProperty(ref T field, T value, [CallerMemberName] string? propertyName = null) + { + if (Equals(field, value)) return false; + field = value; + OnPropertyChanged(propertyName); + return true; + } + } +} diff --git a/Algoloop.Avalonia/ViewModels/ViewModelLocator.cs b/Algoloop.Avalonia/ViewModels/ViewModelLocator.cs new file mode 100644 index 000000000..acd65b4f5 --- /dev/null +++ b/Algoloop.Avalonia/ViewModels/ViewModelLocator.cs @@ -0,0 +1,33 @@ +using CommunityToolkit.Mvvm.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; + +namespace Algoloop.Wpf.ViewModels +{ + public class ViewModelLocator + { + public ViewModelLocator() + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + Ioc.Default.ConfigureServices(services.BuildServiceProvider()); + } + + public static MainViewModel? MainViewModel => + Ioc.Default.GetService(); + public static MarketsViewModel? MarketsViewModel => + Ioc.Default.GetService(); + public static StrategiesViewModel? StrategiesViewModel => + Ioc.Default.GetService(); + public static ResearchViewModel? ResearchViewModel => + Ioc.Default.GetService(); + public static LogViewModel? LogViewModel => + Ioc.Default.GetService(); + public static SettingsViewModel? SettingsViewModel => + Ioc.Default.GetService(); + } +} diff --git a/Algoloop.Avalonia/Views/MainWindow.axaml b/Algoloop.Avalonia/Views/MainWindow.axaml new file mode 100644 index 000000000..ac298c4e0 --- /dev/null +++ b/Algoloop.Avalonia/Views/MainWindow.axaml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Algoloop.Avalonia/Views/MainWindow.axaml.cs b/Algoloop.Avalonia/Views/MainWindow.axaml.cs new file mode 100644 index 000000000..e916e21f9 --- /dev/null +++ b/Algoloop.Avalonia/Views/MainWindow.axaml.cs @@ -0,0 +1,96 @@ +/* + * Copyright 2018 Capnode AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Algoloop.Wpf.Model; +using Algoloop.Wpf.ViewModels; +using Avalonia.Controls; +using Avalonia.Interactivity; +using QuantConnect.Configuration; +using System; +using System.Diagnostics; + +namespace Algoloop.Avalonia.Views +{ + public partial class MainWindow : Window + { + public MainWindow() + { + InitializeComponent(); + + string exeFolder = MainService.GetProgramFolder(); + Config.Set("plugin-directory", exeFolder); + Config.Set("composer-dll-directory", exeFolder); + } + + private void OnFileSettings(object? sender, RoutedEventArgs e) + { + // Settings dialog - to be implemented + if (DataContext is MainViewModel model) + { + model.SaveConfig(); + // TODO: Show settings dialog + model.DoSettings(false); + } + } + + private void OnHelpAbout(object? sender, RoutedEventArgs e) + { + // About dialog - to be implemented + // TODO: Show about dialog + } + + private void OnHelpDocumentation(object? sender, RoutedEventArgs e) + { + OpenUrl("https://github.com/Capnode/Algoloop/wiki/Documentation"); + } + + private void OnHelpTechnicalSupport(object? sender, RoutedEventArgs e) + { + OpenUrl("https://github.com/Capnode/Algoloop/issues"); + } + + private void OnHelpPrivacyPolicy(object? sender, RoutedEventArgs e) + { + OpenUrl("https://github.com/Capnode/Algoloop/wiki/Privacy-policy"); + } + + private static void OpenUrl(string url) + { + try + { + if (OperatingSystem.IsWindows()) + { + url = url.Replace("&", "^&"); + var psi = new ProcessStartInfo("cmd", $"/c start {url}") + { + CreateNoWindow = true + }; + Process.Start(psi); + } + else if (OperatingSystem.IsLinux()) + { + Process.Start("xdg-open", url); + } + else if (OperatingSystem.IsMacOS()) + { + Process.Start("open", url); + } + } + catch (Exception ex) + { + Debug.WriteLine($"{ex.GetType()}: {ex.Message}"); + } + } + } +} diff --git a/Algoloop.Avalonia/app.manifest b/Algoloop.Avalonia/app.manifest new file mode 100644 index 000000000..75b6673a4 --- /dev/null +++ b/Algoloop.Avalonia/app.manifest @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PerMonitorV2 + + true + + + + + + + diff --git a/Algoloop.sln b/Algoloop.sln index 7335e4160..f57f925c8 100644 --- a/Algoloop.sln +++ b/Algoloop.sln @@ -63,6 +63,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Algoloop", "Algoloop\Algolo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuantConnect.DownloaderDataProvider.Launcher", "DownloaderDataProvider\QuantConnect.DownloaderDataProvider.Launcher.csproj", "{387365D5-6937-4211-AA79-CBE46CCB3961}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Algoloop.Avalonia", "Algoloop.Avalonia\Algoloop.Avalonia.csproj", "{B5BCA428-DDD3-466D-89EC-911018CED27E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -667,6 +669,26 @@ Global {387365D5-6937-4211-AA79-CBE46CCB3961}.Release|x64.Build.0 = Release|Any CPU {387365D5-6937-4211-AA79-CBE46CCB3961}.Release|x86.ActiveCfg = Release|Any CPU {387365D5-6937-4211-AA79-CBE46CCB3961}.Release|x86.Build.0 = Release|Any CPU + {B5BCA428-DDD3-466D-89EC-911018CED27E}.Debug|Any CPU.ActiveCfg = Debug|x64 + {B5BCA428-DDD3-466D-89EC-911018CED27E}.Debug|Any CPU.Build.0 = Debug|x64 + {B5BCA428-DDD3-466D-89EC-911018CED27E}.Debug|ARM.ActiveCfg = Debug|x64 + {B5BCA428-DDD3-466D-89EC-911018CED27E}.Debug|ARM.Build.0 = Debug|x64 + {B5BCA428-DDD3-466D-89EC-911018CED27E}.Debug|arm64.ActiveCfg = Debug|x64 + {B5BCA428-DDD3-466D-89EC-911018CED27E}.Debug|arm64.Build.0 = Debug|x64 + {B5BCA428-DDD3-466D-89EC-911018CED27E}.Debug|x64.ActiveCfg = Debug|x64 + {B5BCA428-DDD3-466D-89EC-911018CED27E}.Debug|x64.Build.0 = Debug|x64 + {B5BCA428-DDD3-466D-89EC-911018CED27E}.Debug|x86.ActiveCfg = Debug|x64 + {B5BCA428-DDD3-466D-89EC-911018CED27E}.Debug|x86.Build.0 = Debug|x64 + {B5BCA428-DDD3-466D-89EC-911018CED27E}.Release|Any CPU.ActiveCfg = Release|x64 + {B5BCA428-DDD3-466D-89EC-911018CED27E}.Release|Any CPU.Build.0 = Release|x64 + {B5BCA428-DDD3-466D-89EC-911018CED27E}.Release|ARM.ActiveCfg = Release|x64 + {B5BCA428-DDD3-466D-89EC-911018CED27E}.Release|ARM.Build.0 = Release|x64 + {B5BCA428-DDD3-466D-89EC-911018CED27E}.Release|arm64.ActiveCfg = Release|x64 + {B5BCA428-DDD3-466D-89EC-911018CED27E}.Release|arm64.Build.0 = Release|x64 + {B5BCA428-DDD3-466D-89EC-911018CED27E}.Release|x64.ActiveCfg = Release|x64 + {B5BCA428-DDD3-466D-89EC-911018CED27E}.Release|x64.Build.0 = Release|x64 + {B5BCA428-DDD3-466D-89EC-911018CED27E}.Release|x86.ActiveCfg = Release|x64 + {B5BCA428-DDD3-466D-89EC-911018CED27E}.Release|x86.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE