commit cddf6798a6f7a104bf489fa8d48658f58dfb1baf Author: Mike Nolan Date: Sun Aug 31 00:25:32 2025 -0500 First Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d0ef60 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.vscode +bin +obj +out +build \ No newline at end of file diff --git a/App.axaml b/App.axaml new file mode 100644 index 0000000..b2ca1cf --- /dev/null +++ b/App.axaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + avares://CrossLangDevStudio/Assets/Fonts#Aardvark Fixed Regular + + \ No newline at end of file diff --git a/App.axaml.cs b/App.axaml.cs new file mode 100644 index 0000000..e3c7ddd --- /dev/null +++ b/App.axaml.cs @@ -0,0 +1,47 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; +using System.Linq; +using Avalonia.Markup.Xaml; +using CrossLangDevStudio.ViewModels; +using CrossLangDevStudio.Views; + +namespace CrossLangDevStudio; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + // Avoid duplicate validations from both Avalonia and the CommunityToolkit. + // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins + DisableAvaloniaDataAnnotationValidation(); + desktop.MainWindow = new MainWindow + { + DataContext = new MainWindowViewModel(desktop.Args ?? []), + }; + } + + base.OnFrameworkInitializationCompleted(); + } + + private void DisableAvaloniaDataAnnotationValidation() + { + // Get an array of plugins to remove + var dataValidationPluginsToRemove = + BindingPlugins.DataValidators.OfType().ToArray(); + + // remove each entry found + foreach (var plugin in dataValidationPluginsToRemove) + { + BindingPlugins.DataValidators.Remove(plugin); + } + } +} \ No newline at end of file diff --git a/Assets/Fonts/aardvark-fixed-regular.ttf b/Assets/Fonts/aardvark-fixed-regular.ttf new file mode 100644 index 0000000..14781d7 Binary files /dev/null and b/Assets/Fonts/aardvark-fixed-regular.ttf differ diff --git a/Assets/crosslang.ico b/Assets/crosslang.ico new file mode 100644 index 0000000..3fd66ae Binary files /dev/null and b/Assets/crosslang.ico differ diff --git a/CrossLangDevStudio.csproj b/CrossLangDevStudio.csproj new file mode 100644 index 0000000..da5e124 --- /dev/null +++ b/CrossLangDevStudio.csproj @@ -0,0 +1,31 @@ + + + WinExe + net9.0 + enable + true + app.manifest + true + + + + + + + + + + + + + + + + None + All + + + + + + diff --git a/CrossLangFile.cs b/CrossLangFile.cs new file mode 100644 index 0000000..ddcaf9d --- /dev/null +++ b/CrossLangFile.cs @@ -0,0 +1,407 @@ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace CrossLangDevStudio; + + +public class CrossLangFile +{ + const string CROSSVM = "TCROSSVM"; + public void Load(string path) + { + if (File.Exists(path)) + using (var strm = File.OpenRead(path)) + Load(strm); + } + + + + public void Load(Stream strm) + { + byte[] header = new byte[18]; + strm.ReadExactly(header); + for (int i = 0; i < CROSSVM.Length; i++) + { + if (header[i] != (byte)CROSSVM[i]) throw new Exception("Invalid signature"); + } + + Version = new CrossLangVersion(header, 13); + + uint ReadInt() + { + strm.ReadExactly(header, 0, 4); + + if (BitConverter.IsLittleEndian) + Array.Reverse(header, 0, 4); + return BitConverter.ToUInt32(header, 0); + } + + void ReadString() + { + uint length = ReadInt(); + byte[] data = new byte[length]; + strm.ReadExactly(data, 0, data.Length); + Strings.Add(Encoding.UTF8.GetString(data)); + } + string ReadSectionName() + { + strm.ReadExactly(header, 0, 4); + return Encoding.UTF8.GetString(header, 0, 4); + } + + string GetString() + { + uint index = ReadInt(); + return Strings[(int)index]; + } + + + + uint count = ReadInt(); + for (uint i = 0; i < count; i++) + { + string section = ReadSectionName(); + uint sectionSize = ReadInt(); + switch (section) + { + case "STRS": + { + uint theCount = ReadInt(); + for (uint j = 0; j < theCount; j++) + { + ReadString(); + } + } + break; + case "NAME": + Name = GetString(); + break; + case "INFO": + Info = GetString(); + break; + case "ICON": + Icon = (int)ReadInt(); + break; + case "RESO": + byte[] data = new byte[sectionSize]; + strm.ReadExactly(data, 0, data.Length); + Resources.Add(data); + break; + case "DEPS": + if (sectionSize == 9) + { + string name = GetString(); + strm.ReadExactly(header, 0, 5); + var version = new CrossLangVersion(header); + Dependencies.Add(new CrossLangDependency() { Name = name, Version = version }); + } + break; + default: + strm.Position += sectionSize; + break; + } + } + + } + public string Name { get; private set; } = ""; + public CrossLangVersion Version { get; private set; } + + public string Info { get; private set; } = ""; + + public int Icon { get; private set; } = -1; + + public CrossLangInfo InfoObject => JsonConvert.DeserializeObject(Info) ?? new CrossLangInfo(); + + public Bitmap GetIcon(int height=128) + { + using (Stream strm = (Icon > -1 && Icon < Resources.Count) ? new MemoryStream(Resources[Icon], false) : AssetLoader.Open(new Uri("avares://CrossLangDevStudio/Assets/crosslang.ico"))) + { + return Bitmap.DecodeToHeight(strm, height); + } + } + + internal string GetTemplatePrettyName() + { + var info = InfoObject; + if (info is not null) + { + if (!string.IsNullOrWhiteSpace(info.TemplateNamePretty)) return info.TemplateNamePretty; + if (!string.IsNullOrWhiteSpace(info.TemplateName)) return info.TemplateName; + } + return this.Name; + } + + public List Strings { get; } = new List(); + + public List Resources { get; } = new List(); + + public List Dependencies { get; } = new List(); + +} + + +public class CrossLangConfig +{ + [JsonProperty("name")] + public string? Name { get; set; } + + [JsonProperty("version")] + public CrossLangVersion Version { get; set; } + + [JsonProperty("dependencies")] + public List Dependencies { get; set; } = new List(); + + [JsonProperty("info")] + public CrossLangInfo Info { get; set; } = new CrossLangInfo(); + + [JsonProperty("project_dependencies")] + public List ProjectDependencies { get; set; } = new List(); + + [JsonProperty("icon")] + public string? Icon { get; set; } +} + + +public class CrossLangInfo +{ + [JsonProperty("maintainer")] + public string? Maintainer { get; set; } //Not checked when building project + [JsonProperty("type")] + public string? Type { get; set; } //gets checked whether it is compile_tool by build tool, possible values are in file: project_types.txt + [JsonProperty("repo")] + public string? Repoository { get; set; } //optional, is the place where the code is stored + [JsonProperty("homepage")] + public string? HomePage { get; set; } //optional, is the website for the project + [JsonProperty("license")] + public string? License { get; set; } //optional, but recommended to tell people what the license is + [JsonProperty("description")] + public string? Description { get; set; } //optional but tells people about the package + + [JsonProperty("template_name")] + public string? TemplateName { get; set; } + [JsonProperty("template_name_pretty")] + public string? TemplateNamePretty { get; set; } + + [JsonProperty("template_project_dependencies")] + public JToken? TemplateDependencies { get; set; } + + [JsonProperty("template_info")] + public JToken? TemplateInfo { get; set; } + [JsonProperty("template_extra_text_ftles")] + public JToken? TemplateExtraTextFiles { get; set; } + [JsonProperty("template_ignored_files")] + public JToken? TemplateIgnoredFiles { get; set; } +} + +public class CrossLangDependency +{ + [JsonProperty("name")] + public string Name { get; set; } = ""; + [JsonProperty("verson")] + public CrossLangVersion Version { get; set; } + +} + +public enum CrossLangVersionStage +{ + Development = 0, + Alpha = 1, + Beta = 2, + Production = 3 +} +public class CrossLangVersionConverter : Newtonsoft.Json.JsonConverter +{ + public override CrossLangVersion ReadJson(JsonReader reader, Type objectType, CrossLangVersion existingValue, bool hasExistingValue, JsonSerializer serializer) + { + string? s = reader.Value as string; + if (!string.IsNullOrWhiteSpace(s)) + { + + if (CrossLangVersion.TryParse(s, out var version)) return version; + + } + + if (hasExistingValue) + return existingValue; + return new CrossLangVersion(); + } + + public override void WriteJson(JsonWriter writer, CrossLangVersion value, JsonSerializer serializer) + { + writer.WriteValue(value.ToString()); + } +} +[Newtonsoft.Json.JsonConverter( typeof(CrossLangVersionConverter))] +public struct CrossLangVersion : IComparable, IComparable +{ + public void CopyTo(byte[] data, int offset = 0) + { + if (offset + 5 > data.Length) throw new IndexOutOfRangeException("Cannot write outside of array"); + data[offset + 0] = Major; + data[offset + 1] = Minor; + data[offset + 2] = Patch; + data[offset + 3] = (byte)(BuildAndStage >> 8); + data[offset + 4] = (byte)(BuildAndStage & 0xFF); + } + public byte[] ToArray() + { + byte[] data = new byte[5]; + CopyTo(data); + return data; + } + public static bool TryParse(string str, out CrossLangVersion version) + { + var dashPart = str.Split(new char[] { '-' }, 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + byte major = 1; + byte minor = 0; + byte patch = 0; + ushort build = 0; + CrossLangVersionStage stage = CrossLangVersionStage.Development; + if (dashPart.Length >= 1) + { + + var dotPart = dashPart[0].Split(new char[] { '.' }, 4, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (dotPart.Length > 0) + { + if (!byte.TryParse(dotPart[0], out major)) + { + version = new CrossLangVersion(); + return false; + } + if (dotPart.Length > 1 && !byte.TryParse(dotPart[1], out minor)) + { + version = new CrossLangVersion(); + return false; + } + if (dotPart.Length > 2 && !byte.TryParse(dotPart[2], out patch)) + { + version = new CrossLangVersion(); + return false; + } + if (dotPart.Length > 3 && !ushort.TryParse(dotPart[3], out build)) + { + version = new CrossLangVersion(); + return false; + } + if (dashPart.Length == 2) + { + switch (dashPart[1]) + { + case "dev": + stage = CrossLangVersionStage.Development; + break; + case "alpha": + stage = CrossLangVersionStage.Alpha; + break; + case "beta": + stage = CrossLangVersionStage.Beta; + break; + case "prod": + stage = CrossLangVersionStage.Production; + break; + default: + version = new CrossLangVersion(); + return false; + } + } + + version = new CrossLangVersion(major, minor, patch, build, stage); + return true; + } + + + + + } + + version = new CrossLangVersion(); + + return false; + + + + } + public CrossLangVersion() + { + + } + public CrossLangVersion(byte major, byte minor, byte patch, ushort build, CrossLangVersionStage stage) + { + Major = major; + Minor = minor; + Patch = patch; + Build = build; + Stage = stage; + } + public CrossLangVersion(byte[] data, int offset = 0) + { + if (offset + 5 > data.Length) return; + + Major = data[offset+0]; + Minor = data[offset+1]; + Patch = data[offset+2]; + ushort build = (ushort)(data[offset+3] << 8 | data[offset+4]); + Build = (ushort)(build >> 2); + Stage = (CrossLangVersionStage)(build & 3); + } + public byte Major { get; set; } = 1; + public byte Minor { get; set; } = 0; + public byte Patch { get; set; } = 0; + + public ushort Build { get; set; } = 0; + + public static CrossLangVersion OnePointZeroProd => new CrossLangVersion(){Major=1,Minor=0,Patch=0,Build=0,Stage= CrossLangVersionStage.Production}; + + public CrossLangVersionStage Stage { get; set; } = CrossLangVersionStage.Development; + + public ushort BuildAndStage => (ushort)(Build << 2 | (ushort)Stage); + + public ulong AsInteger => (ulong)((ulong)Major << 32 | (ulong)Minor << 24 | (ulong)Patch << 16 | (ulong)BuildAndStage); + + public int CompareTo(CrossLangVersion other) + { + return AsInteger.CompareTo(other.AsInteger); + } + + public int CompareTo(object? obj) + { + if (obj is CrossLangVersion oth) + return CompareTo(oth); + + return 1; + } + + public override string ToString() + { + string stage="dev"; + switch (Stage) + { + case CrossLangVersionStage.Development: + stage = "dev"; + break; + case CrossLangVersionStage.Alpha: + stage = "alpha"; + break; + case CrossLangVersionStage.Beta: + stage = "beta"; + break; + case CrossLangVersionStage.Production: + stage = "prod"; + break; + } + return $"{Major}.{Minor}.{Patch}.{Build}-{stage}"; + } +} \ No newline at end of file diff --git a/CrossLangShell.cs b/CrossLangShell.cs new file mode 100644 index 0000000..9ed29c6 --- /dev/null +++ b/CrossLangShell.cs @@ -0,0 +1,270 @@ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Security.Cryptography.X509Certificates; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace CrossLangDevStudio; + + +class CrossLangShell +{ + private static CrossLangSettings? settings = null; + public static CrossLangSettings Settings + { + get + { + if (settings != null) return settings; + var config = CrossLangConfigDirectory; + if (!string.IsNullOrWhiteSpace(config)) + { + var devStudioDir = Path.Combine(config, "DevStudio"); + Directory.CreateDirectory(devStudioDir); + var settingsFile = Path.Combine(devStudioDir, "Settings.json"); + if (File.Exists(settingsFile)) + { + settings = JsonConvert.DeserializeObject(File.ReadAllText(settingsFile)); + } + } + + return settings is null ? settings = new CrossLangSettings() : settings; + } + } + private static string _crosslang_config_dir = ""; + + public static string CrossLangConfigDirectory => string.IsNullOrWhiteSpace(_crosslang_config_dir) ? _crosslang_config_dir = GetCrossLangDirectory() : _crosslang_config_dir; + + public static List GetRecentProjects() + { + var config = CrossLangConfigDirectory; + if (string.IsNullOrWhiteSpace(config)) return new List(); + + var devStudioDir = Path.Combine(config, "DevStudio"); + Directory.CreateDirectory(devStudioDir); + var configPath = Path.Combine(devStudioDir, "Recents.json"); + if (File.Exists(configPath)) + { + var list = JsonConvert.DeserializeObject>(File.ReadAllText(configPath)); + if (list is not null) return list; + } + + return new List(); + } + public static void OpenFolderInFileManager(string path) + { + using (Process p = new Process()) + { + p.StartInfo.FileName = path; + p.StartInfo.UseShellExecute = true; + p.Start(); + } + } + public static void EnsureRecent(string projectPath) + { + projectPath = Path.GetFullPath(projectPath).TrimEnd(Path.DirectorySeparatorChar); + var recents = GetRecentProjects(); + if (recents.Contains(projectPath)) + { + recents.Remove(projectPath); + } + recents.Insert(0, projectPath); + if (recents.Count > 5) + { + recents.RemoveRange(5, recents.Count - 5); + } + + var config = CrossLangConfigDirectory; + if (string.IsNullOrWhiteSpace(config)) return; + + var devStudioDir = Path.Combine(config, "DevStudio"); + Directory.CreateDirectory(devStudioDir); + var configPath = Path.Combine(devStudioDir, "Recents.json"); + File.WriteAllText(configPath, JsonConvert.SerializeObject(recents, Formatting.Indented)); + } + public static string GetRealPath(string exec) + { + foreach (var item in (Environment.GetEnvironmentVariable("PATH") ?? "").Split(Path.PathSeparator)) + { + var path = Path.Combine(item, exec); + if (File.Exists(path)) return path; + foreach (var ext in (Environment.GetEnvironmentVariable("PATHEXT") ?? "").Split(Path.PathSeparator)) + { + if (File.Exists(path + ext)) return path + ext; + } + } + return ""; + } + + public static string CrossLangPath + { + get + { + var path = GetRealPath("crosslang"); + return path; + } + } + public static bool HaveCrossLang + { + get + { + return !string.IsNullOrWhiteSpace(CrossLangPath); + } + } + public static void BuildProjectInFolder(string folder) + { + OpenTerminal(false, folder, CrossLangPath, "build"); + } + public static void RunProjectInFolder(string folder) + { + OpenTerminal(true, folder, CrossLangPath, "run"); + } + public static void OpenTerminalInFolder(string folder) + { + if (OperatingSystem.IsLinux()) + { + OpenTerminal(false, folder, "", []); + } + } + + public static void OpenTerminal(bool keepOpen, string workingDirectory, string commandName, params string[] args) + { + var preferedTerminalCommand = Settings.PreferedTerminalCommand; + if (!string.IsNullOrWhiteSpace(preferedTerminalCommand)) + { + var firstArgs = preferedTerminalCommand.Split(" "); + using Process process = new(); + process.StartInfo.UseShellExecute = false; + process.StartInfo.WorkingDirectory = workingDirectory; + process.StartInfo.FileName = firstArgs[0]; + for (int i = 1; i < firstArgs.Length; i++) + { + process.StartInfo.ArgumentList.Add(firstArgs[i]); + } + foreach (var arg in args) + process.StartInfo.ArgumentList.Add(arg); + process.Start(); + return; + } + if (OperatingSystem.IsLinux() || OperatingSystem.IsFreeBSD()) + { + string konsole = GetRealPath("konsole"); + string gnome_terminal = GetRealPath("gnome-terminal"); + + string xterm = GetRealPath("xterm"); + if (File.Exists(konsole)) + { + using Process process = new Process(); + process.StartInfo.WorkingDirectory = workingDirectory; + process.StartInfo.FileName = konsole; + process.StartInfo.UseShellExecute = false; + if (commandName.Length != 0) + { + if (keepOpen) + process.StartInfo.ArgumentList.Add("--hold"); + process.StartInfo.ArgumentList.Add("-e"); + process.StartInfo.ArgumentList.Add(commandName); + foreach (var arg in args) + { + process.StartInfo.ArgumentList.Add(arg); + } + } + process.Start(); + } + else if (File.Exists(gnome_terminal)) + { + + } + else if (File.Exists(xterm)) + { + + } + } + else if (OperatingSystem.IsWindows()) + { + using Process process = new Process(); + process.StartInfo.FileName = GetRealPath("cmd.exe"); + if(commandName.Length > 0) + process.StartInfo.ArgumentList.Add(keepOpen ? "/K" : "/C"); + process.StartInfo.CreateNoWindow = false; + process.StartInfo.UseShellExecute = false; + process.StartInfo.WorkingDirectory = workingDirectory; + if(commandName.Length > 0) + process.StartInfo.ArgumentList.Add(commandName); + foreach (var item in args) + process.StartInfo.ArgumentList.Add(item); + process.Start(); + } + } + + + private static string GetCrossLangDirectory() + { + string path = CrossLangPath; + if (string.IsNullOrWhiteSpace(path)) return ""; + using Process process = new Process(); + process.StartInfo.ArgumentList.Add("configdir"); + process.StartInfo.FileName = path; + process.StartInfo.CreateNoWindow = true; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.UseShellExecute = false; + process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; + if (process.Start()) + { + + process.WaitForExit(); + var text = process.StandardOutput.ReadToEnd(); + return (text ?? "").Replace("\n", "").Replace("\r", ""); + } + return ""; + } + + internal static bool CreateProject(string templateName, string path) + { + string cpath = CrossLangPath; + if (string.IsNullOrWhiteSpace(cpath)) return false; + using Process process = new Process(); + process.StartInfo.ArgumentList.Add("new"); + process.StartInfo.ArgumentList.Add(templateName); + process.StartInfo.WorkingDirectory = path; + process.StartInfo.FileName = cpath; + process.StartInfo.CreateNoWindow = true; + + process.StartInfo.UseShellExecute = false; + process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; + if (process.Start()) + { + + process.WaitForExit(); + return process.ExitCode == 0; + } + return false; + } +} + +public class CrossLangSettings +{ + [JsonProperty("prefered_terminal_command")] + public string PreferedTerminalCommand { get; set; } = ""; + [JsonProperty("prefered_project_directory")] + public string PreferedProjectDirectory { get; set; } = ""; + [Newtonsoft.Json.JsonIgnore] + public string ProjectDirectory + { + get + { + if (!string.IsNullOrWhiteSpace(PreferedProjectDirectory)) + return PreferedProjectDirectory; + + string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "CrossLangProjects"); + Directory.CreateDirectory(path); + return path; + } + } + + +} \ No newline at end of file diff --git a/Messages/GetWindowMessage.cs b/Messages/GetWindowMessage.cs new file mode 100644 index 0000000..444cc07 --- /dev/null +++ b/Messages/GetWindowMessage.cs @@ -0,0 +1,7 @@ +using CrossLangDevStudio.ViewModels; +using CommunityToolkit.Mvvm.Messaging.Messages; +using Avalonia.Controls; + +namespace CrossLangDevStudio.Messages; + +public class GetWindowMessage : AsyncRequestMessage; \ No newline at end of file diff --git a/Messages/NewProjectCloseMessage.cs b/Messages/NewProjectCloseMessage.cs new file mode 100644 index 0000000..8768129 --- /dev/null +++ b/Messages/NewProjectCloseMessage.cs @@ -0,0 +1,9 @@ +using CrossLangDevStudio.ViewModels; +using CommunityToolkit.Mvvm.Messaging.Messages; + +namespace CrossLangDevStudio.Messages; + +public class NewProjectCloseMessage(string? path) : AsyncRequestMessage +{ + public string? Path => path; +} \ No newline at end of file diff --git a/Messages/NewProjectMessage.cs b/Messages/NewProjectMessage.cs new file mode 100644 index 0000000..a54cca3 --- /dev/null +++ b/Messages/NewProjectMessage.cs @@ -0,0 +1,6 @@ +using CrossLangDevStudio.ViewModels; +using CommunityToolkit.Mvvm.Messaging.Messages; + +namespace CrossLangDevStudio.Messages; + +public class NewProjectMessage : AsyncRequestMessage; \ No newline at end of file diff --git a/Models/ProjectFileNode.cs b/Models/ProjectFileNode.cs new file mode 100644 index 0000000..47025b2 --- /dev/null +++ b/Models/ProjectFileNode.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.ObjectModel; +using Avalonia.Controls; + +namespace CrossLangDevStudio.Models; + +public class ProjectFileNode +{ + public ObservableCollection? SubNodes { get; } + public string Title { get; } + + public Action? Click { get; } + + public ProjectFileNode(string title, Action? click = null) + { + Title = title; + Click = click; + } + + public ProjectFileNode(string title, ObservableCollection subNodes) + { + Title = title; + SubNodes = subNodes; + } +} \ No newline at end of file diff --git a/Packaging/Linux/build.sh b/Packaging/Linux/build.sh new file mode 100644 index 0000000..e69de29 diff --git a/Packaging/Windows/Toolchain.cmake b/Packaging/Windows/Toolchain.cmake new file mode 100644 index 0000000..444308d --- /dev/null +++ b/Packaging/Windows/Toolchain.cmake @@ -0,0 +1,12 @@ + + set(CMAKE_C_COMPILER "/usr/bin/i686-w64-mingw32-gcc") + set(CMAKE_CXX_COMPILER "/usr/bin/i686-w64-mingw32-g++") + set(CMAKE_C_FLAGS "-static-libgcc -static-libstdc++ -static") + set(CMAKE_CXX_FLAGS "-static-libgcc -static-libstdc++ -static") + set(CMAKE_EXE_LINKER_FLAGS "-static-libgcc -static-libstdc++ -static") + + set(CMAKE_SYSROOT "/home/mike/dvd/Working/Sysroot/windows") + + set(CMAKE_SYSTEM_NAME Windows) + set(CMAKE_SYSTEM_PROCESSOR i686) + diff --git a/Packaging/Windows/build.sh b/Packaging/Windows/build.sh new file mode 100644 index 0000000..d82fdb8 --- /dev/null +++ b/Packaging/Windows/build.sh @@ -0,0 +1,19 @@ +#!/bin/bash\ +mkdir build +cd build +git clone https://onedev.site.tesses.net/crosslang +cd crosslang +mkdir build +cd build +cmake -S .. -B . -DCMAKE_TOOLCHAIN_FILE=../../../Toolchain.cmake -DTESSESFRAMEWORK_ENABLE_SHARED=OFF -DTESSESFRAMEWORK_FETCHCONTENT=ON +make -j`nproc` +mkdir -p ../../package/bin +mkdir -p ../../package/share/Tesses/CrossLang +cp crosslang.exe ../../package/bin +cp ../winicon.ico ../../package/crosslang.ico +cd ../.. +wget -O package/share/Tesses/CrossLang/Tesses.CrossLang.ShellPackage-1.0.0.0-prod.crvm https://downloads.tesses.net/ShellPackage.crvm +cd package +dotnet publish -c Release -r win-x64 -o bin -p:PublishReadyToRun=true -p:PublishSingleFile=true --self-contained ../../../../CrossLangDevStudio.csproj +cp ../../script.nsh . +makensis script.nsh \ No newline at end of file diff --git a/Packaging/Windows/license.txt b/Packaging/Windows/license.txt new file mode 100644 index 0000000..e69de29 diff --git a/Packaging/Windows/script.nsh b/Packaging/Windows/script.nsh new file mode 100644 index 0000000..4c971cf --- /dev/null +++ b/Packaging/Windows/script.nsh @@ -0,0 +1,147 @@ +# This installs two files, app.exe and logo.ico, creates a start menu shortcut, builds an uninstaller, and +# adds uninstall information to the registry for Add/Remove Programs + +# To get started, put this script into a folder with the two files (app.exe, logo.ico, and license.rtf - +# You'll have to create these yourself) and run makensis on it + +# If you change the names "app.exe", "logo.ico", or "license.rtf" you should do a search and replace - they +# show up in a few places. +# All the other settings can be tweaked by editing the !defines at the top of this script +!define APPNAME "CrossLang DevStudio" +!define COMPANYNAME "Tesses" +!define DESCRIPTION "IDE for CrossLang" +# These three must be integers +!define VERSIONMAJOR 1 +!define VERSIONMINOR 0 +!define VERSIONBUILD 0 +# These will be displayed by the "Click here for support information" link in "Add/Remove Programs" +# It is possible to use "mailto:" links in here to open the email client +!define ABOUTURL "https://crosslang.tesseslanguage.com/" # "Publisher" link +# This is the size (in kB) of all the files copied into "Program Files" + + +RequestExecutionLevel admin ;Require admin rights on NT6+ (When UAC is turned on) + +InstallDir "$PROGRAMFILES\${COMPANYNAME}\${APPNAME}" + +# rtf or txt file - remember if it is txt, it must be in the DOS text format (\r\n) +LicenseData "license.txt" +# This will be in the installer/uninstaller's title bar +Name "${COMPANYNAME} - ${APPNAME}" +Icon "crosslang.ico" +outFile "CrossLangDevStudio-Installer.exe" + +!include LogicLib.nsh + +# Just three pages - license agreement, install location, and installation +page license +page directory +Page instfiles + +!macro VerifyUserIsAdmin +UserInfo::GetAccountType +pop $0 +${If} $0 != "admin" ;Require admin rights on NT4+ + messageBox mb_iconstop "Administrator rights required!" + setErrorLevel 740 ;ERROR_ELEVATION_REQUIRED + quit +${EndIf} +!macroend + +function .onInit + setShellVarContext all + !insertmacro VerifyUserIsAdmin +functionEnd + +section "install" + createDirectory "$INSTDIR\bin" + createDirectory "$INSTDIR\share" + createDirectory "$INSTDIR\share\Tesses" + createDirectory "$INSTDIR\share\Tesses\CrossLang" + # Files for the install directory - to build the installer, these should be in the same directory as the install script (this file) + setOutPath $INSTDIR + + # Files added here should be removed by the uninstaller (see section "uninstall") + + file "crosslang.ico" + setOutPath $INSTDIR\bin + + file "bin\CrossLangDevStudio.exe" + file "bin\av_libglesv2.dll" + file "bin\libHarfBuzzSharp.dll" + file "bin\libSkiaSharp.dll" + file "bin\crosslang.exe" + setOutPath $INSTDIR\share\Tesses\CrossLang + + file "share\Tesses\CrossLang\Tesses.CrossLang.ShellPackage-1.0.0.0-prod.crvm" + + # Add any other files for the install directory (license files, app data, etc) here + + # Uninstaller - See function un.onInit and section "uninstall" for configuration + writeUninstaller "$INSTDIR\uninstall.exe" + + # Start Menu + createDirectory "$SMPROGRAMS\${COMPANYNAME}" + createShortCut "$SMPROGRAMS\${COMPANYNAME}\${APPNAME}.lnk" "$INSTDIR\bin\CrossLangDevStudio.exe" "" "$INSTDIR\crosslang.ico" + + # Registry information for add/remove programs + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "DisplayName" "${COMPANYNAME} - ${APPNAME} - ${DESCRIPTION}" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "InstallLocation" "$\"$INSTDIR$\"" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "DisplayIcon" "$\"$INSTDIR\logo.ico$\"" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "Publisher" "$\"${COMPANYNAME}$\"" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "URLInfoAbout" "$\"${ABOUTURL}$\"" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "DisplayVersion" "$\"${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONBUILD}$\"" + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "VersionMajor" ${VERSIONMAJOR} + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "VersionMinor" ${VERSIONMINOR} + # There is no option for modifying or repairing the install + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "NoModify" 1 + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "NoRepair" 1 + WriteRegStr HKCR "Directory\Background\shell\${APPNAME}\command" "" "$\"$INSTDIR\bin\CrossLangDevStudio.exe$\" $\"%V$\"" +sectionEnd + +# Uninstaller + +function un.onInit + SetShellVarContext all + + #Verify the uninstaller - last chance to back out + MessageBox MB_OKCANCEL "Permanantly remove ${APPNAME}?" IDOK next + Abort + next: + !insertmacro VerifyUserIsAdmin +functionEnd + +section "uninstall" + + # Remove Start Menu launcher + delete "$SMPROGRAMS\${COMPANYNAME}\${APPNAME}.lnk" + # Try to remove the Start Menu folder - this will only happen if it is empty + rmDir "$SMPROGRAMS\${COMPANYNAME}" + + # Remove files + delete $INSTDIR\bin\CrossLangDevStudio.exe + delete $INSTDIR\crosslang.ico + + delete $INSTDIR\bin\av_libglesv2.dll + delete $INSTDIR\bin\libHarfBuzzSharp.dll + delete $INSTDIR\bin\libSkiaSharp.dll + delete $INSTDIR\bin\crosslang.exe + + # Always delete uninstaller as the last action + delete $INSTDIR\uninstall.exe + delete $INSTDIR\share\Tesses\CrossLang\Tesses.CrossLang.ShellPackage-1.0.0.0-prod.crvm + rmDir "$INSTDIR\share\Tesses\CrossLang" + rmDir "$INSTDIR\share\Tesses" + rmDir "$INSTDIR\bin" + rmDir "$INSTDIR\share" + + + + # Try to remove the install directory - this will only happen if it is empty + rmDir $INSTDIR + + # Remove uninstaller information from the registry + DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" +sectionEnd \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..25faf55 --- /dev/null +++ b/Program.cs @@ -0,0 +1,21 @@ +using Avalonia; +using System; + +namespace CrossLangDevStudio; + +sealed class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); +} diff --git a/ViewLocator.cs b/ViewLocator.cs new file mode 100644 index 0000000..70a9738 --- /dev/null +++ b/ViewLocator.cs @@ -0,0 +1,31 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using CrossLangDevStudio.ViewModels; + +namespace CrossLangDevStudio; + +public class ViewLocator : IDataTemplate +{ + + public Control? Build(object? param) + { + if (param is null) + return null; + + var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); + var type = Type.GetType(name); + + if (type != null) + { + return (Control)Activator.CreateInstance(type)!; + } + + return new TextBlock { Text = "Not Found: " + name }; + } + + public bool Match(object? data) + { + return data is ViewModelBase; + } +} diff --git a/ViewModels/FileEditorViewModel.cs b/ViewModels/FileEditorViewModel.cs new file mode 100644 index 0000000..6e3c52a --- /dev/null +++ b/ViewModels/FileEditorViewModel.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Platform.Storage; + +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using CrossLangDevStudio.Messages; +using CrossLangDevStudio.Views; + +namespace CrossLangDevStudio.ViewModels; + +public partial class FileEditorViewModel : ViewModelBase, ISavable +{ + TabItemViewModel tab; + public string FilePath { get; } + bool _modified = false; + private bool Modified + { + get => _modified; + set + { + _modified = value; + + if (value) + { + tab.Header = $"{Path.GetFileName(FilePath)}*"; + } + else + { + tab.Header = Path.GetFileName(FilePath); + } + } + } + + private string _text = ""; + + public string Text + { + get => _text; + set + { + + Modified = true; + this.SetProperty(ref _text, value, nameof(Text)); + } + } + + public FileEditorViewModel(string path,TabItemViewModel tab) + { + this.tab = tab; + FilePath = path; + _text = File.ReadAllText(path); + } + + public void Save() + { + File.WriteAllText(FilePath, Text); + Modified = false; + } +} \ No newline at end of file diff --git a/ViewModels/ISavable.cs b/ViewModels/ISavable.cs new file mode 100644 index 0000000..0335067 --- /dev/null +++ b/ViewModels/ISavable.cs @@ -0,0 +1,5 @@ +namespace CrossLangDevStudio.ViewModels; +interface ISavable +{ + void Save(); +} \ No newline at end of file diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..714480b --- /dev/null +++ b/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,409 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Platform.Storage; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using CrossLangDevStudio.Messages; +using CrossLangDevStudio.Models; +using CrossLangDevStudio.Views; + +namespace CrossLangDevStudio.ViewModels; + +public partial class MainWindowViewModel : ViewModelBase +{ + + public HttpClient Client { get; set; } = new(); + public string CurrentProject { get; set; } = ""; + public Func NewItemFactory => AddItem; + + + public ObservableCollection TabItems { get; } = new(); + public ObservableCollection ProjectFiles { get; } = new ObservableCollection(); + + public EventHandler? Closed { get; } = null; + + public TabItemViewModel WelcomePage + { + get + { + return new TabItemViewModel() + { + Header = "Welcome", + Body = new WelcomeViewModel(this) + }; + } + } + private ProjectFileNode? _selectedProjectFile = null; + + public ProjectFileNode? SelectedProjectFile + { + get => _selectedProjectFile; + set + { + _selectedProjectFile = value; + value?.Click?.Invoke(); + } + } + + + public MainWindowViewModel(string[] args) + { + if (args.Length == 1) + { + LoadProject(args[0]); + } + if (string.IsNullOrWhiteSpace(CurrentProject)) + this.TabItems.Add(WelcomePage); + } + + + private object AddItem() + { + var tab = new TabItemViewModel + { + Header = "New Tab", + + }; + if (Directory.Exists(CurrentProject)) + { + var newTabItem = new NewTabViewModel(this, tab); + tab.Body = newTabItem; + } + + + + return tab; + } + + internal void LoadProject(string obj) + { + if (!File.Exists(Path.Combine(obj, "cross.json"))) + { + return; + } + CrossLangShell.EnsureRecent(obj); + CurrentProject = obj.TrimEnd(Path.DirectorySeparatorChar); + Refresh(CurrentProject); + } + [RelayCommand] + public void RefreshListing() + { + if (!File.Exists(Path.Combine(CurrentProject, "cross.json"))) + { + return; + } + Refresh(CurrentProject); + } + + private void Refresh(string obj) + { + + + + for (int i = 0; i < TabItems.Count; i++) + { + if (TabItems[i].Body is WelcomeViewModel) + { + TabItems.RemoveAt(i); + i--; + } + + } + + ObservableCollection entries = new ObservableCollection + { + new ProjectFileNode("Project",[ + new ProjectFileNode("Configuration",()=>{ + + OpenProjectConfig(); + }), + new ProjectFileNode("Packages") + ]) + + }; + + void Itterate(ObservableCollection nodes, string dir) + { + + foreach (var item in Directory.EnumerateDirectories(dir)) + { + ObservableCollection files = new(); + Itterate(files, item); + var pfn = new ProjectFileNode(Path.GetFileName(item), files); + nodes.Add(pfn); + } + foreach (var item in Directory.EnumerateFiles(dir)) + { + nodes.Add(new ProjectFileNode(Path.GetFileName(item), () => + { + OpenFile(item); + })); + } + + } + Itterate(entries, obj); + ProjectFiles.Clear(); + ProjectFiles.Add(new ProjectFileNode(Path.GetFileName(obj), entries)); + } + + private void OpenProjectConfig() + { + + if (Directory.Exists(CurrentProject)) + { + var config = Path.Combine(CurrentProject, "cross.json"); + foreach (var item in TabItems) + { + if (item.Body is ProjectConfigurationViewModel model && model.FilePath == config) + { + return; + } + } + TabItemViewModel vm = new TabItemViewModel(); + vm.Header = "Project Configuration"; + var pcm = new ProjectConfigurationViewModel(config, vm); + + vm.Body = pcm; + + AddTab(vm); + } + } + + static string[] FILE_EXTS = new string[]{ + ".tcross", + ".json", + ".txt", + ".cpp", + ".hpp", + ".html", + ".css", + ".webmanifest", + ".cs", + ".c", + ".h", + ".xml", + ".xaml", + ".js", + ".jsx", + ".ts", + ".tsx", + ".gitignore", + ".svg" + }; + + public void OpenFile(string path) + { + bool isValid = false; + foreach (var item in FILE_EXTS) + { + if (Path.GetExtension(path).ToLower() == item) + isValid = true; + } + if (!isValid) return; + + foreach (var item in TabItems) + { + if (item.Body is FileEditorViewModel model) + { + if (model.FilePath == path) + { + SelectedTab = item; + return; + } + } + } + var tab = new TabItemViewModel + { + Header = Path.GetFileName(path), + + }; + tab.Body = new FileEditorViewModel(path,tab); + AddTab(tab); + } + + [ObservableProperty] + private TabItemViewModel? _selectedTab = null; + private void AddTab(TabItemViewModel item) + { + TabItems.Add(item); + SelectedTab = item; + + } + [RelayCommand] + public async Task NewProjectAsync() + { + var res = await WeakReferenceMessenger.Default.Send(new NewProjectMessage()); + if (!string.IsNullOrWhiteSpace(res)) + { + LoadProject(res); + } + } + [RelayCommand] + public async Task OpenProjectAsync() + { + var opts = new FolderPickerOpenOptions(); + opts.AllowMultiple = false; + opts.Title = "Open Project"; + + var window = await WeakReferenceMessenger.Default.Send(new GetWindowMessage()); + opts.SuggestedStartLocation = await window.StorageProvider.TryGetFolderFromPathAsync(CrossLangShell.Settings.ProjectDirectory) ?? null; + var res = await window.StorageProvider.OpenFolderPickerAsync(opts); + if (res.Count == 1) + { + var url = res[0].TryGetLocalPath(); + if (!string.IsNullOrWhiteSpace(url)) + { + LoadProject(url); + } + } + } + + [RelayCommand] + private void OpenProjectInFileManager() + { + if (Directory.Exists(CurrentProject)) + { + CrossLangShell.OpenFolderInFileManager(CurrentProject); + } + } + [RelayCommand] + private void OpenProjectInTerminal() + { + if (Directory.Exists(CurrentProject)) + { + CrossLangShell.OpenTerminalInFolder(CurrentProject); + } + } + [RelayCommand] + private void BuildAndRun() + { + SaveAll(); + if (Directory.Exists(CurrentProject)) + { + CrossLangShell.RunProjectInFolder(CurrentProject); + } + } + [RelayCommand] + private void Build() + { + SaveAll(); + if (Directory.Exists(CurrentProject)) + { + CrossLangShell.BuildProjectInFolder(CurrentProject); + } + } + + [RelayCommand] + private void Save() + { + SelectedTab?.Save(); + } + [RelayCommand] + private void SaveAll() + { + foreach (var tab in TabItems) + { + tab.Save(); + } + } +} + +public partial class NewTabViewModel : ViewModelBase +{ + private MainWindowViewModel mainWindowViewModel; + private TabItemViewModel tab; + + public NewTabViewModel(MainWindowViewModel mainWindowViewModel, TabItemViewModel tab) + { + this.mainWindowViewModel = mainWindowViewModel; + this.tab = tab; + } + + [ObservableProperty] + private string _filePath = "src/"; + + [ObservableProperty] + private decimal _progress = 0; + + [ObservableProperty] + private string _url = ""; + + [RelayCommand] + private void CreateDirectory() + { + if (!string.IsNullOrWhiteSpace(FilePath)) + { + Directory.CreateDirectory(Path.Combine(mainWindowViewModel.CurrentProject, FilePath)); + mainWindowViewModel.RefreshListing(); + mainWindowViewModel.TabItems.Remove(tab); + } + } + [RelayCommand] + private void CreateFile() + { + if (!string.IsNullOrWhiteSpace(FilePath)) + { + var path = Path.Combine(mainWindowViewModel.CurrentProject, FilePath); + File.WriteAllText(path, ""); + mainWindowViewModel.RefreshListing(); + mainWindowViewModel.TabItems.Remove(tab); + mainWindowViewModel.OpenFile(path); + } + } + + [RelayCommand] + private async Task DownloadFileAsync() + { + try + { + using var destStrm = File.Create(Path.Combine(mainWindowViewModel.CurrentProject, FilePath)); + var res = await mainWindowViewModel.Client.GetAsync(Url); + long length = 0; + long offset = 0; + Progress = 0; + if (res.IsSuccessStatusCode) + { + if (res.Content.Headers.ContentLength.HasValue) + { + length = res.Content.Headers.ContentLength.Value; + byte[] array = new byte[1024]; + int read = 0; + using var src = await res.Content.ReadAsStreamAsync(); + do + { + read = await src.ReadAsync(array); + await destStrm.WriteAsync(array, 0, read); + offset += read; + if (length > 0) + { + Progress = (decimal)offset / (decimal)length; + } + + } while (read != 0); + } + else + { + await res.Content.CopyToAsync(destStrm); + } + Progress = 1; + mainWindowViewModel.RefreshListing(); + mainWindowViewModel.TabItems.Remove(tab); + } + else + { + throw new Exception(); + } + } + catch (Exception) + { + File.Delete(FilePath); + this.Url = $"FAILED: {this.Url}"; + } + + } +} \ No newline at end of file diff --git a/ViewModels/NewProjectDialogViewModel.cs b/ViewModels/NewProjectDialogViewModel.cs new file mode 100644 index 0000000..1d1a076 --- /dev/null +++ b/ViewModels/NewProjectDialogViewModel.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using Avalonia.Markup.Xaml.Templates; +using Avalonia.Media; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using CrossLangDevStudio.Messages; +using CrossLangDevStudio.Views; + +namespace CrossLangDevStudio.ViewModels; + +public partial class NewProjectDialogViewModel : ViewModelBase +{ + public ObservableCollection CrossLangTemplates { get; set; } = new ObservableCollection(); + bool updated = false; + string _name = ""; + public string Name + { + get + { + return _name; + } + set + { + updated = true; + SetProperty(ref _name, value, nameof(Name)); + } + } + + private void SelectProjectType(string name) + { + + + if (!updated) + { + var newName = name.Replace(" ", ""); + int i; + for (i = 1; i < int.MaxValue; i++) + { + string name2 = $"{name}{i}"; + if (!Directory.Exists(Path.Combine(ParentDirectory, name2))) + { + Name = name2; + updated = false; + break; + } + } + } + } + + [ObservableProperty] + private string _parentDirectory = CrossLangShell.Settings.ProjectDirectory; + CrossLangTemplate? _template = null; + public CrossLangTemplate? SelectedTemplate + { + get + { + return _template; + } + set + { + SetProperty(ref _template, value, nameof(SelectedTemplate)); + if (value is not null) + { + SelectProjectType(value.Name); + } + } + } + + public NewProjectDialogViewModel() + { + foreach (var item in Directory.EnumerateFiles(Path.Combine(CrossLangShell.CrossLangConfigDirectory, "Templates"), "*.crvm")) + { + CrossLangFile file = new CrossLangFile(); + try + { + file.Load(item); + CrossLangTemplates.Add(new CrossLangTemplate(Path.GetFileNameWithoutExtension(item), file)); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + } + } + + [RelayCommand] + private void OK() + { + if (_template is not null && !string.IsNullOrWhiteSpace(Name) && !string.IsNullOrWhiteSpace(ParentDirectory)) + { + var path = Path.Combine(ParentDirectory, Name); + Directory.CreateDirectory(path); + if (CrossLangShell.CreateProject(_template.TemplateName, path)) + { + WeakReferenceMessenger.Default.Send(new NewProjectCloseMessage(path)); + } + + } + } + [RelayCommand] + private void Cancel() + { + WeakReferenceMessenger.Default.Send(new NewProjectCloseMessage(null)); + } +} + +public partial class CrossLangTemplate(string name,CrossLangFile file) : ObservableObject +{ + + [ObservableProperty] + private string _name = file.GetTemplatePrettyName(); + [ObservableProperty] + private IImage _icon = file.GetIcon(64); + + public string TemplateName = name; + +} \ No newline at end of file diff --git a/ViewModels/ProjectConfigurationViewModel.cs b/ViewModels/ProjectConfigurationViewModel.cs new file mode 100644 index 0000000..8f75f5b --- /dev/null +++ b/ViewModels/ProjectConfigurationViewModel.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Platform.Storage; + +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using CrossLangDevStudio.Messages; +using CrossLangDevStudio.Views; +using Newtonsoft.Json; + +namespace CrossLangDevStudio.ViewModels; + +public partial class ProjectConfigurationViewModel : ViewModelBase, ISavable +{ + /* + Console Application + Library + Web Application + Template + Compile Tool + Tool + Archive + */ + static readonly string[] types = ["console","lib","app","template","compile_tool","tool","archive"]; + TabItemViewModel tab; + public string FilePath { get; } + bool _modified = false; + private bool Modified + { + get => _modified; + set + { + _modified = value; + + if (value) + { + tab.Header = $"Project Configuration*"; + } + else + { + tab.Header = "Project Configuration"; + } + } + } + + string _name; + string _version; + string _icon; + string _maintainer; + string _repository; + string _homepage; + + string _license; + + string _templatename; + string _templatenamepretty; + + string _description; + + int _type; + + + public string Name + { + get => _name; + set + { + Modified = true; + SetProperty(ref _name, value, nameof(Name)); + } + } + public string Version + { + get => _version; + set + { + Modified = true; + SetProperty(ref _version, value, nameof(Version)); + } + } + + public string Icon + { + get => _icon; + set + { + Modified = true; + SetProperty(ref _icon, value, nameof(Icon)); + } + } + public string Maintainer + { + get => _maintainer; + set + { + Modified = true; + SetProperty(ref _maintainer, value, nameof(Maintainer)); + } + } + public string Homepage + { + get => _homepage; + set + { + Modified = true; + SetProperty(ref _homepage, value, nameof(Homepage)); + } + } + public string Repository + { + get => _repository; + set + { + Modified = true; + SetProperty(ref _repository, value, nameof(Repository)); + } + } + + public string License + { + get => _license; + set + { + Modified = true; + SetProperty(ref _license, value, nameof(License)); + } + } + + public string TemplateName + { + get => _templatename; + set + { + Modified = true; + SetProperty(ref _templatename, value, nameof(TemplateName)); + } + } + public string TemplateNamePretty + { + get => _templatenamepretty; + set + { + Modified = true; + SetProperty(ref _templatenamepretty, value, nameof(TemplateNamePretty)); + } + } + public string Description + { + get => _description; + set + { + Modified = true; + SetProperty(ref _description, value, nameof(Description)); + } + } + + public int Type + { + get => _type; + set + { + Modified = true; + SetProperty(ref _type, value, nameof(Type)); + } + } + + public ProjectConfigurationViewModel(string configPath, TabItemViewModel tab) + { + FilePath = configPath; + this.tab = tab; + + + var config = JsonConvert.DeserializeObject(File.ReadAllText(configPath)); + + _name = config?.Name ?? ""; + _version = config?.Version.ToString() ?? "1.0.0.0-dev"; + _icon = config?.Icon ?? ""; + _repository = config?.Info?.Repoository ?? ""; + _homepage = config?.Info?.HomePage ?? ""; + _maintainer = config?.Info?.Maintainer ?? ""; + _license = config?.Info?.License ?? ""; + _templatename = config?.Info?.TemplateName ?? ""; + _templatenamepretty = config?.Info?.TemplateNamePretty ?? ""; + _type = Array.IndexOf(types, config?.Info?.Type ?? "console"); + _type = _type == -1 ? 0 : _type; + _description = config?.Info?.Description ?? ""; + + } + [RelayCommand] + + public void Save() + { + var config = JsonConvert.DeserializeObject(File.ReadAllText(FilePath)) ?? new CrossLangConfig(); + config.Name = Name; + if (CrossLangVersion.TryParse(Version, out var vers)) + { + config.Version = vers; + } + config.Icon = string.IsNullOrWhiteSpace(Icon) ? null : Icon; + + config.Info.Description = string.IsNullOrWhiteSpace(Description) ? null : Description; + config.Info.HomePage = string.IsNullOrWhiteSpace(Homepage) ? null : Homepage; + config.Info.License = string.IsNullOrWhiteSpace(License) ? null : License; + config.Info.Maintainer = string.IsNullOrWhiteSpace(Maintainer) ? null : Maintainer; + config.Info.Repoository = string.IsNullOrWhiteSpace(Repository) ? null : Repository; + config.Info.TemplateName = string.IsNullOrWhiteSpace(TemplateName) ? null : TemplateName; + config.Info.TemplateNamePretty = string.IsNullOrWhiteSpace(TemplateNamePretty) ? null : TemplateNamePretty; + config.Info.Type = types[Type]; + File.WriteAllText(FilePath, JsonConvert.SerializeObject(config, Formatting.Indented, new JsonSerializerSettings() + { + NullValueHandling = NullValueHandling.Ignore + })); + Modified = false; + } +} \ No newline at end of file diff --git a/ViewModels/TabItemViewModel.cs b/ViewModels/TabItemViewModel.cs new file mode 100644 index 0000000..21521fc --- /dev/null +++ b/ViewModels/TabItemViewModel.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace CrossLangDevStudio.ViewModels; + +public partial class TabItemViewModel : ObservableObject +{ + [ObservableProperty] + private string _header = ""; + + public ViewModelBase? Body { get; set; } + + + public override string ToString() => Header; + + public void Save() + { + if (Body is ISavable savable) + { + savable.Save(); + } + } +} \ No newline at end of file diff --git a/ViewModels/ViewModelBase.cs b/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..1fbcbc9 --- /dev/null +++ b/ViewModels/ViewModelBase.cs @@ -0,0 +1,7 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace CrossLangDevStudio.ViewModels; + +public class ViewModelBase : ObservableObject +{ +} diff --git a/ViewModels/WelcomeViewModel.cs b/ViewModels/WelcomeViewModel.cs new file mode 100644 index 0000000..c979814 --- /dev/null +++ b/ViewModels/WelcomeViewModel.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Platform.Storage; + +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using CrossLangDevStudio.Messages; +using CrossLangDevStudio.Views; + +namespace CrossLangDevStudio.ViewModels; + +public partial class WelcomeViewModel : ViewModelBase +{ + public MainWindowViewModel Main { get; } + public WelcomeViewModel(MainWindowViewModel main) + { + Main = main; + foreach (var proj in CrossLangShell.GetRecentProjects()) + { + RecentProjects.Add(new RecentProject(proj,Main.LoadProject)); + } + } + + + + public ObservableCollection RecentProjects { get; set; } = new ObservableCollection(); + + [RelayCommand] + private async Task NewProjectAsync() + { + await Main.NewProjectAsync(); + } + [RelayCommand] + private async Task OpenProjectAsync() + { + await Main.OpenProjectAsync(); + } +} + +public partial class RecentProject(string path, Action onProjectClicked) : ObservableObject +{ + public string ProjectPath { get; set; } = path; + + private string GetDir() + { + var projPath = ProjectPath; + var path = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + if (projPath.StartsWith(path)) + { + projPath= projPath.Replace(path, "~"); + } + return projPath; + } + + + public string Name => $"{Path.GetFileName(ProjectPath)} ({GetDir()})"; + [RelayCommand] + private void Click() + { + onProjectClicked(ProjectPath); + } +} \ No newline at end of file diff --git a/Views/FileEditorView.axaml b/Views/FileEditorView.axaml new file mode 100644 index 0000000..4a799e2 --- /dev/null +++ b/Views/FileEditorView.axaml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/Views/FileEditorView.axaml.cs b/Views/FileEditorView.axaml.cs new file mode 100644 index 0000000..2312b5b --- /dev/null +++ b/Views/FileEditorView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; +using CrossLangDevStudio.ViewModels; + +namespace CrossLangDevStudio.Views; + +public partial class FileEditorView : UserControl +{ + public FileEditorView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Views/MainWindow.axaml b/Views/MainWindow.axaml new file mode 100644 index 0000000..1f16f10 --- /dev/null +++ b/Views/MainWindow.axaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Views/MainWindow.axaml.cs b/Views/MainWindow.axaml.cs new file mode 100644 index 0000000..5905c2c --- /dev/null +++ b/Views/MainWindow.axaml.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Platform.Storage; +using CommunityToolkit.Mvvm.Messaging; +using CrossLangDevStudio.Messages; +using CrossLangDevStudio.ViewModels; + +namespace CrossLangDevStudio.Views; + +public partial class MainWindow : Window +{ + public MainWindow() + { + InitializeComponent(); + + if (Design.IsDesignMode) + return; + + // Whenever 'Send(new PurchaseAlbumMessage())' is called, invoke this callback on the MainWindow instance: + WeakReferenceMessenger.Default.Register(this, static (w, m) => + { + // Create an instance of MusicStoreWindow and set MusicStoreViewModel as its DataContext. + var dialog = new NewProjectDialog + { + DataContext = new NewProjectDialogViewModel() + }; + // Show dialog window and reply with returned AlbumViewModel or null when the dialog is closed. + m.Reply(dialog.ShowDialog(w)); + }); + WeakReferenceMessenger.Default.Register(this, static (w, m) => + { + m.Reply(w); + } + ); + + } +} \ No newline at end of file diff --git a/Views/NewProjectDialog.axaml b/Views/NewProjectDialog.axaml new file mode 100644 index 0000000..2b2a7d2 --- /dev/null +++ b/Views/NewProjectDialog.axaml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +