diff --git a/Installer/SkEditorInstaller.wixproj b/Installer/SkEditorInstaller.wixproj index cbd4b8ab..1a983db0 100644 --- a/Installer/SkEditorInstaller.wixproj +++ b/Installer/SkEditorInstaller.wixproj @@ -4,6 +4,7 @@ + \ No newline at end of file diff --git a/SkEditor/Assets/Icons.axaml b/SkEditor/Assets/Icons.axaml index d9b0175d..d8ec5e7e 100644 --- a/SkEditor/Assets/Icons.axaml +++ b/SkEditor/Assets/Icons.axaml @@ -44,5 +44,10 @@ + + + + + \ No newline at end of file diff --git a/SkEditor/Controls/Sidebar/ExplorerSidebarPanel.axaml b/SkEditor/Controls/Sidebar/ExplorerSidebarPanel.axaml index de19b46f..1dd2f21e 100644 --- a/SkEditor/Controls/Sidebar/ExplorerSidebarPanel.axaml +++ b/SkEditor/Controls/Sidebar/ExplorerSidebarPanel.axaml @@ -3,6 +3,9 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:ui="using:FluentAvalonia.UI.Controls" + xmlns:ar="clr-namespace:SkEditor.Utilities.Projects" + xmlns:elements="clr-namespace:SkEditor.Utilities.Projects.Elements" + xmlns:visualBasic="clr-namespace:Microsoft.VisualBasic;assembly=Microsoft.VisualBasic.Core" x:Class="SkEditor.Controls.Sidebar.ExplorerSidebarPanel"> @@ -26,12 +29,81 @@ - + - - + + + Loading XX files ... + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + You don't have any folders opened yet. Keep in mind they are in beta! + + diff --git a/SkEditor/Controls/Sidebar/ExplorerSidebarPanel.axaml.cs b/SkEditor/Controls/Sidebar/ExplorerSidebarPanel.axaml.cs index 2ef46737..24f09777 100644 --- a/SkEditor/Controls/Sidebar/ExplorerSidebarPanel.axaml.cs +++ b/SkEditor/Controls/Sidebar/ExplorerSidebarPanel.axaml.cs @@ -1,8 +1,8 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; +using Avalonia.Controls; +using Avalonia.Interactivity; using FluentAvalonia.UI.Controls; using SkEditor.Utilities; +using SkEditor.Utilities.Projects; namespace SkEditor.Controls.Sidebar; @@ -21,4 +21,9 @@ public class ExplorerPanel : SidebarPanel public readonly ExplorerSidebarPanel Panel = new (); } + + private void OpenFolder(object? sender, RoutedEventArgs e) + { + ProjectOpener.OpenProject(); + } } \ No newline at end of file diff --git a/SkEditor/Languages/English.xaml b/SkEditor/Languages/English.xaml index 52281c21..4d7464a8 100644 --- a/SkEditor/Languages/English.xaml +++ b/SkEditor/Languages/English.xaml @@ -13,6 +13,8 @@ Generate command Select item Marketplace + Rename + Create File @@ -46,11 +48,19 @@ GUI Command Refactor - + Marketplace Refresh syntax + + Open in explorer + Copy relative path + Copy absolute path + Rename + New file + New folder + Apply Confirm @@ -98,6 +108,21 @@ Convert spaces to tabs Renaming '{0}' into ... + + + Explorer + Projects are still in beta and bugs may occur! + Open Folder + Renaming into ... + Name cannot be empty. + Parent cannot be null. + A file or folder with this name already exists. + The new name is the same as the old name. + A file or folder with this name already exists. + File Name + Folder Name + Template + Select file association There's multiple provider for this file type. Please select one. @@ -106,6 +131,7 @@ Remember my choice Confirm + Enabled Disabled diff --git a/SkEditor/SkEditor.csproj b/SkEditor/SkEditor.csproj index 991b81b2..f9965cd8 100644 --- a/SkEditor/SkEditor.csproj +++ b/SkEditor/SkEditor.csproj @@ -46,6 +46,7 @@ + diff --git a/SkEditor/Utilities/Editor/CustomCommandsHandler.cs b/SkEditor/Utilities/Editor/CustomCommandsHandler.cs index 485d0d24..c8b0eabd 100644 --- a/SkEditor/Utilities/Editor/CustomCommandsHandler.cs +++ b/SkEditor/Utilities/Editor/CustomCommandsHandler.cs @@ -18,23 +18,39 @@ public static void OnCommentCommandExecuted(object target) var document = editor.Document; var selectionStart = editor.SelectionStart; var selectionLength = editor.SelectionLength; + var indentation = editor.Options.IndentationString; var selectedLines = document.Lines .Where(line => selectionStart <= line.EndOffset && selectionStart + selectionLength >= line.Offset) .ToList(); - bool allLinesCommented = selectedLines.All(line => document.GetText(line).StartsWith("#")); - var modifiedLines = selectedLines.Select(line => { var text = document.GetText(line); - if (allLinesCommented) + if (string.IsNullOrWhiteSpace(text)) + return text; + + // Find the first non-tabulator character + var strippedLine = text.TrimStart(); + var isCommented = text.TrimStart().StartsWith("#"); + var indentationAmount = 0; + while (text.StartsWith(indentation)) { - return text.StartsWith('#') ? text[1..] : text; + text = text[indentation.Length..]; + indentationAmount++; } - else + + string indentationToInsert = ""; + for (int i = 0; i < indentationAmount; i++) + indentationToInsert += indentation; + + ApiVault.Get().Log("Indentation 2 insert: " + indentationToInsert + " | Line: '" + text + "'", true); + if (isCommented) + { + return indentationToInsert + strippedLine[1..]; + } else { - return text.StartsWith('#') ? "##" + text[1..] : "#" + text; + return indentationToInsert + "#" + strippedLine; } }).ToList(); diff --git a/SkEditor/Utilities/Files/Icon.cs b/SkEditor/Utilities/Files/Icon.cs index 29adfd02..2b230e39 100644 --- a/SkEditor/Utilities/Files/Icon.cs +++ b/SkEditor/Utilities/Files/Icon.cs @@ -28,4 +28,17 @@ public static void SetIcon(TabViewItem tabViewItem) tabViewItem.IconSource = iconSource; } + + public static IconSource? GetIcon(string extension) + { + string iconName = IconDictionary.GetValueOrDefault(extension); + + if (iconName is not null) + { + Application.Current.TryGetResource(iconName, Avalonia.Styling.ThemeVariant.Default, out object icon); + return icon as PathIconSource; + } + + return null; + } } diff --git a/SkEditor/Utilities/Projects/Elements/File.cs b/SkEditor/Utilities/Projects/Elements/File.cs new file mode 100644 index 00000000..aec079dc --- /dev/null +++ b/SkEditor/Utilities/Projects/Elements/File.cs @@ -0,0 +1,88 @@ +using System.Diagnostics; +using System.IO; +using System.Linq; +using Avalonia.Platform.Storage; +using CommunityToolkit.Mvvm.Input; +using SkEditor.API; +using SkEditor.Utilities.Files; + +namespace SkEditor.Utilities.Projects.Elements; + +public class File : StorageElement +{ + + public string StorageFilePath { get; set; } + + public File(string file, Folder? parent = null) + { + Parent = parent; + StorageFilePath = file; + + Name = Path.GetFileName(file); + IsFile = true; + + var icon = Files.Icon.GetIcon(Path.GetExtension(file)); + if (icon is not null) + Icon = icon; + + // Commands + OpenInExplorerCommand = new RelayCommand(OpenInExplorer); + DeleteCommand = new RelayCommand(DeleteFile); + CopyAbsolutePathCommand = new RelayCommand(CopyAbsolutePath); + CopyPathCommand = new RelayCommand(CopyPath); + } + + public void OpenInExplorer() + { + Process.Start(new ProcessStartInfo(Parent.StorageFolderPath) { UseShellExecute = true }); + } + + public void DeleteFile() + { + System.IO.File.Delete(StorageFilePath); + Parent.Children.Remove(this); + } + + public override string? ValidateName(string input) + { + if (input == Name) + return Translation.Get("ProjectRenameErrorSameName"); + + if (Parent is null) + return Translation.Get("ProjectRenameErrorParentNull"); + + var file = Parent.Children.FirstOrDefault(x => x.Name == input); + if (file is not null) + return Translation.Get("ProjectRenameErrorNameExists"); + + return null; + } + + public override void RenameElement(string newName) + { + var newPath = Path.Combine(Parent.StorageFolderPath, newName); + + System.IO.File.Move(StorageFilePath, newPath); + StorageFilePath = newPath; + + Name = newName; + + RefreshSelf(); + } + + public override void HandleDoubleClick() + { + FileHandler.OpenFile(StorageFilePath); + } + + public void CopyAbsolutePath() + { + ApiVault.Get().GetMainWindow().Clipboard.SetTextAsync(StorageFilePath.Replace("\\", "/")); + } + + public void CopyPath() + { + var path = StorageFilePath.Replace(ProjectOpener.ProjectRootFolder.StorageFolderPath, ""); + ApiVault.Get().GetMainWindow().Clipboard.SetTextAsync(path.Replace("\\", "/")); + } +} \ No newline at end of file diff --git a/SkEditor/Utilities/Projects/Elements/Folder.cs b/SkEditor/Utilities/Projects/Elements/Folder.cs new file mode 100644 index 00000000..05ec7be2 --- /dev/null +++ b/SkEditor/Utilities/Projects/Elements/Folder.cs @@ -0,0 +1,141 @@ +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Avalonia.Platform.Storage; +using CommunityToolkit.Mvvm.Input; +using SkEditor.API; +using SkEditor.Utilities.Files; +using SkEditor.Views; +using SkEditor.Views.Projects; + +namespace SkEditor.Utilities.Projects.Elements; + +public class Folder : StorageElement +{ + + public string StorageFolderPath { get; set; } + + public Folder(string folder, Folder? parent = null) + { + Parent = parent; + StorageFolderPath = folder; + Name = Path.GetFileName(folder); + IsFile = false; + + Children = new ObservableCollection(); + LoadChildren(); + + // Commands + OpenInExplorerCommand = new RelayCommand(OpenInExplorer); + DeleteCommand = new RelayCommand(DeleteFolder); + CopyPathCommand = new RelayCommand(CopyPath); + CopyAbsolutePathCommand = new RelayCommand(CopyAbsolutePath); + CreateNewFileCommand = new RelayCommand(() => CreateNewElement(true)); + CreateNewFolderCommand = new RelayCommand(() => CreateNewElement(false)); + } + + private void LoadChildren() + { + foreach (var child in Directory.GetDirectories(StorageFolderPath)) + Children.Add(new Folder(child, this)); + + foreach (var child in Directory.GetFiles(StorageFolderPath)) + Children.Add(new File(child, this)); + } + + public void OpenInExplorer() + { + Process.Start(new ProcessStartInfo(StorageFolderPath) { UseShellExecute = true }); + } + + public void DeleteFolder() + { + Directory.Delete(StorageFolderPath, true); + Parent.Children.Remove(this); + } + + public override void RenameElement(string newName) + { + var newPath = Path.Combine(Parent.StorageFolderPath, newName); + Directory.Move(StorageFolderPath, newPath); + StorageFolderPath = newPath; + Name = newName; + + RefreshSelf(); + } + + public override string? ValidateName(string input) + { + if (input == Name) + return Translation.Get("ProjectRenameErrorSameName"); + + if (Parent is null) + return Translation.Get("ProjectRenameErrorParentNull"); + + var folder = Parent.Children.FirstOrDefault(x => x.Name == input); + if (folder is not null) + return Translation.Get("ProjectRenameErrorNameExists"); + + return null; + } + + public string? ValidateCreationName(string input) + { + if (string.IsNullOrWhiteSpace(input)) + return Translation.Get("ProjectCreateErrorNameEmpty"); + + if (Children.Any(x => x.Name == input)) + return Translation.Get("ProjectCreateErrorNameExists"); + + return null; + } + + public override void HandleDoubleClick() + { + if (Children.Count == 0) + return; + + IsExpanded = !IsExpanded; + } + + public void CopyAbsolutePath() + { + ApiVault.Get().GetMainWindow().Clipboard.SetTextAsync(StorageFolderPath.Replace("\\", "/")); + } + + public void CopyPath() + { + var path = StorageFolderPath.Replace(ProjectOpener.ProjectRootFolder.StorageFolderPath, ""); + ApiVault.Get().GetMainWindow().Clipboard.SetTextAsync(path.Replace("\\", "/")); + } + + public async void CreateNewElement(bool file) + { + var window = new CreateStorageElementWindow(this, file); + await window.ShowDialog(MainWindow.Instance); + } + + public async void CreateFile(string name) + { + var path = Path.Combine(StorageFolderPath, name); + + System.IO.File.Create(path).Close(); + FileHandler.OpenFile(path); + + var element = new File(path, this); + Children.Add(element); + Sort(this); + } + + public void CreateFolder(string name) + { + var path = Path.Combine(StorageFolderPath, name); + + Directory.CreateDirectory(path); + + var element = new Folder(path, this); + Children.Add(element); + Sort(this); + } +} \ No newline at end of file diff --git a/SkEditor/Utilities/Projects/Elements/StorageElement.cs b/SkEditor/Utilities/Projects/Elements/StorageElement.cs new file mode 100644 index 00000000..a5c2f2be --- /dev/null +++ b/SkEditor/Utilities/Projects/Elements/StorageElement.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using AvaloniaEdit.Utils; +using CommunityToolkit.Mvvm.Input; +using FluentAvalonia.UI.Controls; +using SkEditor.Views; +using SkEditor.Views.Projects; + +namespace SkEditor.Utilities.Projects.Elements; + +public abstract class StorageElement +{ + public ObservableCollection? Children { get; set; } + public Folder? Parent { get; set; } + + public string Name { get; set; } = ""; + public bool IsExpanded { get; set; } = false; + + public bool IsFile { get; set; } + + public IconSource Icon { get; set; } = new SymbolIconSource() { Symbol = Symbol.Document, FontSize = 24}; + + public RelayCommand OpenInExplorerCommand { get; set; } + public RelayCommand RenameCommand => new (OpenRenameWindow); + public RelayCommand DeleteCommand { get; set; } + public RelayCommand DoubleClickCommand => new (HandleDoubleClick); + public RelayCommand CopyPathCommand { get; set; } + public RelayCommand CopyAbsolutePathCommand { get; set; } + public RelayCommand CreateNewFileCommand { get; set; } + public RelayCommand CreateNewFolderCommand { get; set; } + + public abstract string? ValidateName(string input); + + public abstract void RenameElement(string newName); + + public abstract void HandleDoubleClick(); + + public async void OpenRenameWindow() + { + var window = new RenameElementWindow(this); + await window.ShowDialog(MainWindow.Instance); + } + + public void RefreshSelf() + { + Parent.Children[Parent.Children.IndexOf(this)] = this; + Sort(Parent); + } + + public void Sort(StorageElement element) + { + if (element.Children != null) + { + var temp = element.Children.ToList(); + element.Children.Clear(); + element.Children.AddRange(temp.OrderBy(x => x.IsFile).ThenBy(x => x.Name)); + } + } +} \ No newline at end of file diff --git a/SkEditor/Utilities/Projects/ProjectOpener.cs b/SkEditor/Utilities/Projects/ProjectOpener.cs index 337ad96c..b72e5564 100644 --- a/SkEditor/Utilities/Projects/ProjectOpener.cs +++ b/SkEditor/Utilities/Projects/ProjectOpener.cs @@ -1,18 +1,24 @@ -using Avalonia.Controls; -using Avalonia.Media; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Linq; +using Avalonia.Controls; using Avalonia.Platform.Storage; +using CommunityToolkit.Mvvm.Input; +using FluentAvalonia.UI.Controls; using SkEditor.API; -using SkEditor.Utilities.Files; -using System; -using System.Collections.Generic; -using System.IO; +using SkEditor.Controls.Sidebar; +using SkEditor.Utilities.Projects.Elements; namespace SkEditor.Utilities.Projects; public static class ProjectOpener { - private static TreeView FileTreeView => ApiVault.Get().GetMainWindow().SideBar.ProjectPanel.Panel.FileTreeView; + public static Folder? ProjectRootFolder = null; + private static ExplorerSidebarPanel Panel => ApiVault.Get().GetMainWindow().SideBar.ProjectPanel.Panel; + private static TreeView FileTreeView => Panel.FileTreeView; + private static StackPanel NoFolderMessage => Panel.NoFolderMessage; - public async static void OpenProject() + public static async void OpenProject() { TopLevel topLevel = TopLevel.GetTopLevel(ApiVault.Get().GetMainWindow()); @@ -22,60 +28,150 @@ public async static void OpenProject() AllowMultiple = false }); - if (folder.Count == 0) return; - - FileTreeView.Items.Clear(); + if (folder.Count == 0) + { + NoFolderMessage.IsVisible = true; + return; + } + + NoFolderMessage.IsVisible = false; + + ProjectRootFolder = new Folder(folder[0].Path.AbsolutePath) + { IsExpanded = true }; + FileTreeView.ItemsSource = new ObservableCollection { ProjectRootFolder }; + } + + #region ContextMenu Creation + + private static MenuFlyout CreateContextMenu(TreeViewItem treeViewItem, IStorageItem storageItem) + { + var commands = new[] + { + new { Header = "MenuHeaderNewFile", Command = new RelayCommand(() => CreateElement(treeViewItem, storageItem)), Icon = Symbol.New }, + new { Header = "MenuHeaderNewFolder", Command = new RelayCommand(() => CreateElement(treeViewItem, storageItem, true)), Icon = Symbol.NewFolder }, + new { Header = "MenuHeaderOpenInExplorer", Command = new RelayCommand(() => OpenInExplorer(storageItem)), Icon = Symbol.OpenLocal }, + null, + new { Header = "MenuHeaderCopyPath", Command = new RelayCommand(() => CopyPath(storageItem)), Icon = Symbol.Copy }, + new { Header = "MenuHeaderCopyAbsolutePath", Command = new RelayCommand(() => CopyPath(storageItem, true)), Icon = Symbol.Copy }, + null, + new { Header = "MenuHeaderRename", Command = new RelayCommand(() => DeleteItem(storageItem)), Icon = Symbol.Rename }, + new { Header = "MenuHeaderDelete", Command = new RelayCommand(() => DeleteItem(storageItem)), Icon = Symbol.Delete } + }; - foreach (IStorageFolder storageFolder in folder) + var contextMenu = new MenuFlyout(); + List list = new List(); + foreach (var item in commands) { - TreeViewItem rootFolder = new() - { - Header = storageFolder.Name, - Tag = storageFolder.Path, - IsExpanded = true, - FontWeight = FontWeight.SemiBold - }; + if (item == null) + list.Add(new Separator()); + else + list.Add(new MenuItem { Header = Translation.Get(item.Header), Command = item.Command, Icon = new SymbolIcon { Symbol = item.Icon, FontSize = 20 } }); + } - FileTreeView.Items.Add(rootFolder); + list.ForEach(item => contextMenu.Items.Add(item)); - AddChildren(rootFolder, storageFolder); + return contextMenu; + } + + private static async void DeleteItem(IStorageItem storageItem) + { + if (storageItem is IStorageFolder storageFolder) + { + await storageFolder.DeleteAsync(); + + var parent = FileTreeView.SelectedItem as TreeViewItem; + parent.Items.Remove(parent); + + SortTabItem(parent); + } + else + { + await storageItem.DeleteAsync(); + + var parent = FileTreeView.SelectedItem as TreeViewItem; + parent.Items.Remove(parent); + + SortTabItem(parent); } } - - public async static void AddChildren(TreeViewItem viewItem, IStorageFolder folder) + + private static void CreateElement(TreeViewItem treeViewItem, + IStorageItem storageItem, bool isFolder = false) { - await foreach (IStorageItem storageItem in folder.GetItemsAsync()) + if (storageItem is IStorageFolder storageFolder) { - string path = Uri.UnescapeDataString(storageItem.Path.AbsolutePath); - if (storageItem is IStorageFolder storageFolder) + if (!treeViewItem.IsExpanded) + treeViewItem.IsExpanded = true; + + /* + TreeViewItem item = CreateInputItem(treeViewItem, async (name) => { - TreeViewItem folderItem = new() - { - Header = Path.GetFileName(path), - Tag = path, - FontWeight = FontWeight.Medium, - }; + await Task.Delay(10).ContinueWith(_ => Dispatcher.UIThread.InvokeAsync(() => treeViewItem.IsExpanded = true)); - viewItem.Items.Add(folderItem); + IStorageItem newStorageItem; + if (isFolder) + newStorageItem = await storageFolder.CreateFolderAsync(name); + else + newStorageItem = await storageFolder.CreateFileAsync(name); - AddChildren(folderItem, storageFolder); - } - else + TreeViewItem createdViewItem = CreateTreeViewItem(newStorageItem); + treeViewItem.IsExpanded = true; + treeViewItem.Items.Add(createdViewItem); + + if (newStorageItem is IStorageFile) + FileHandler.OpenFile(newStorageItem.Path.AbsolutePath); + }); + + treeViewItem.Items.Insert(0, item); + item.Focus(NavigationMethod.Pointer); */ + } + } + + private static async void CopyPath(IStorageItem storageItem, bool absolutePath = false) + { + if (absolutePath) + { + await ApiVault.Get().GetMainWindow().Clipboard.SetTextAsync(storageItem.Path.AbsolutePath); + return; + } + + IStorageFolder folder = storageItem as IStorageFolder; + while (folder != null && folder.Path.AbsolutePath != ProjectRootFolder?.StorageFolderPath) + folder = await folder.GetParentAsync(); + + if (folder == null) + return; + + string path = storageItem.Path.AbsolutePath.Replace(folder.Path.AbsolutePath, ""); + await ApiVault.Get().GetMainWindow().Clipboard.SetTextAsync(path); + } + + private static void OpenInExplorer(IStorageItem storageItem) + { + if (storageItem is IStorageFolder storageFolder) + { + var path = storageFolder.Path.AbsolutePath; + Process.Start(new ProcessStartInfo { - TreeViewItem item = new() - { - Header = Path.GetFileName(path), - Tag = path, - FontWeight = FontWeight.Normal - }; + FileName = path, + UseShellExecute = true + }); + } + } - item.DoubleTapped += (sender, e) => - { - FileHandler.OpenFile(path); - }; + #endregion - viewItem.Items.Add(item); - } - } + #region Sorting + + private static void SortTabItem(TreeViewItem parent) + { + var folders = parent.Items.OfType().Where(item => item.Tag is IStorageFolder).OrderBy(item => item.Header); + var files = parent.Items.OfType().Where(item => item.Tag is IStorageFile).OrderBy(item => item.Header); + + parent.Items.Clear(); + foreach (var folder in folders) parent.Items.Add(folder); + foreach (var file in files) parent.Items.Add(file); } + + #endregion } diff --git a/SkEditor/Views/Projects/CreateStorageElementWindow.axaml b/SkEditor/Views/Projects/CreateStorageElementWindow.axaml new file mode 100644 index 00000000..f4b58f79 --- /dev/null +++ b/SkEditor/Views/Projects/CreateStorageElementWindow.axaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + File Name + + + + File Template + + + + + + +