Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 149 additions & 7 deletions src/SharpIDE.Godot/Features/SolutionExplorer/SolutionExplorerPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
using SharpIDE.Godot.Features.BottomPanel;
using SharpIDE.Godot.Features.Common;
using SharpIDE.Godot.Features.Git;
using SharpIDE.Godot.Features.Problems;

namespace SharpIDE.Godot.Features.SolutionExplorer;

Expand All @@ -31,20 +30,27 @@ public partial class SolutionExplorerPanel : MarginContainer
public Texture2D UnloadedProjectIcon { get; set; } = null!;
[Export]
public Texture2D SlnIcon { get; set; } = null!;
[Export]
public StyleBoxFlat SearchMatchHighlight { get; set; } = null!;

public SharpIdeSolutionModel SolutionModel { get; set; } = null!;
private PanelContainer _panelContainer = null!;
private Tree _tree = null!;
private TreeItem _rootItem = null!;
private LineEdit _searchInput = null!;

private readonly Dictionary<TreeItem, bool> _treeItemCollapsedStates = [];

private enum ClipboardOperation { Cut, Copy }

private (List<IFileOrFolder>, ClipboardOperation)? _itemsOnClipboard;
public override void _Ready()
{
_panelContainer = GetNode<PanelContainer>("PanelContainer");
_panelContainer = GetNode<PanelContainer>("%TreeContainer");
_tree = GetNode<Tree>("%Tree");
_tree.ItemMouseSelected += TreeOnItemMouseSelected;
_searchInput = GetNode<LineEdit>("%SearchInput");
_searchInput.TextChanged += OnSearchInputChanged;
// Remove the tree from the scene tree for now, we will add it back when we bind to a solution
_panelContainer.RemoveChild(_tree);
GodotGlobalEvents.Instance.FileExternallySelected.Subscribe(OnFileExternallySelected);
Expand Down Expand Up @@ -74,7 +80,114 @@ public override void _UnhandledKeyInput(InputEvent @event)
else if (@event is InputEventKey { Pressed: true, Keycode: Key.Escape })
{
ClearSlnExplorerClipboard();
HideSearch();
}
else if (@event.IsActionPressed(InputStringNames.FindInSolutionExplorer, exactMatch: true))
{
if (!IsSearchActive())
ShowSearch();
else
HideSearch();

AcceptEvent();
}
}

private void OnSearchInputChanged(string newText)
{
if (!IsSearchActive() || string.IsNullOrWhiteSpace(newText))
{
RestoreTreeItemCollapsedStates(_rootItem);
ShowEntireTree(_rootItem);
ScrollToSelectedTreeItem();
}
else
{
FilterTree(_rootItem, newText);
}

_tree.QueueRedraw();
}

private static void ShowEntireTree(TreeItem item)
{
item.Visible = true;
foreach (var child in item.GetChildren())
{
ShowEntireTree(child);
}
}

private static bool FilterTree(TreeItem item, string searchText)
{
var itemText = item.GetText(0);
var isMatch = itemText.Contains(searchText, StringComparison.OrdinalIgnoreCase);

var hasMatchingChild = false;
foreach (var child in item.GetChildren())
{
if (FilterTree(child, searchText))
hasMatchingChild = true;
}

item.Visible = isMatch || hasMatchingChild;
item.Collapsed = !hasMatchingChild;

return isMatch || hasMatchingChild;
}

private bool IsSearchActive() => _searchInput.IsVisible();

private void ShowSearch()
{
if (IsSearchActive()) return;

_treeItemCollapsedStates.Clear();
SaveTreeItemCollapsedStates(_rootItem);
_searchInput.GrabFocus(hideFocus: true);
_searchInput.Show();
if (!string.IsNullOrWhiteSpace(_searchInput.Text))
FilterTree(_rootItem, _searchInput.Text);
}

private void HideSearch()
{
if (!IsSearchActive()) return;

_searchInput.Hide();
RestoreTreeItemCollapsedStates(_rootItem);
ShowEntireTree(_rootItem);
ScrollToSelectedTreeItem();
}

private void SaveTreeItemCollapsedStates(TreeItem item)
{
_treeItemCollapsedStates[item] = item.Collapsed;

foreach (var child in item.GetChildren())
{
SaveTreeItemCollapsedStates(child);
}
}

private void RestoreTreeItemCollapsedStates(TreeItem item)
{
// If an item was selected during the search then we want to keep it uncollapsed, otherwise we restore it to the state before the search.
item.Collapsed = !HasSelectedChild(item) && _treeItemCollapsedStates.TryGetValue(item, out var collapsed) && collapsed;

foreach (var child in item.GetChildren())
{
RestoreTreeItemCollapsedStates(child);
}
}

private static bool HasSelectedChild(TreeItem item) => item.GetChildren().Any(child => child.IsSelected(0) || HasSelectedChild(child));

private void ScrollToSelectedTreeItem()
{
if (_tree.GetSelected() is not { } selected) return;

_tree.ScrollToItem(selected, centerOnItem: true);
}

private void TreeOnItemMouseSelected(Vector2 mousePosition, long mouseButtonIndex)
Expand Down Expand Up @@ -160,7 +273,7 @@ public async Task BindToSolution(SharpIdeSolutionModel solution)
_tree.Clear();

// Root
var rootItem = _tree.CreateItem();
var rootItem = CreateTreeItem();
rootItem.SetText(0, solution.Name);
rootItem.SetIcon(0, SlnIcon);
_rootItem = rootItem;
Expand Down Expand Up @@ -195,10 +308,39 @@ await this.InvokeAsync(() =>
});
}

private TreeItem CreateTreeItem(TreeItem? parent = null, int index = -1)
{
var item = _tree.CreateItem(parent, index);
item.SetCellMode(0, TreeItem.TreeCellMode.Custom);
item.SetCustomDrawCallback(0, Callable.From<TreeItem, Rect2>(TreeItemCustomDraw));
return item;
}

private void TreeItemCustomDraw(TreeItem item, Rect2 rect)
{
if (!IsSearchActive() || string.IsNullOrWhiteSpace(_searchInput.Text)) return;

var text = item.GetText(0);
var matchIndex = text.FindN(_searchInput.Text);

if (matchIndex < 0) return;

var icon = item.GetIcon(0);
var font = _tree.GetThemeFont(ThemeStringNames.Font);
var fontSize = _tree.GetThemeFontSize(ThemeStringNames.FontSize);
var separation = _tree.GetThemeConstant(ThemeStringNames.HSeparation);
var textMatchX = separation + font.GetStringSize(text.Left(matchIndex), HorizontalAlignment.Left, width: -1f, fontSize).X;
var highlightPosition = new Vector2(rect.Position.X + textMatchX + (icon?.GetWidth() ?? 0), rect.Position.Y);
var highlightSize = new Vector2(font.GetStringSize(_searchInput.Text, HorizontalAlignment.Left, width: -1f, fontSize).X, rect.Size.Y);

var highlightRect = new Rect2(highlightPosition, highlightSize);
_tree.DrawStyleBox(SearchMatchHighlight, highlightRect);
}

[RequiresGodotUiThread]
private TreeItem CreateSlnFolderTreeItem(Tree tree, TreeItem parent, SharpIdeSolutionFolder slnFolder)
{
var folderItem = tree.CreateItem(parent);
var folderItem = CreateTreeItem(parent);
folderItem.SetText(0, slnFolder.Name);
folderItem.SetIcon(0, SlnFolderIcon);
folderItem.SharpIdeNode = slnFolder;
Expand Down Expand Up @@ -240,7 +382,7 @@ private TreeItem CreateSlnFolderTreeItem(Tree tree, TreeItem parent, SharpIdeSol
[RequiresGodotUiThread]
private TreeItem CreateProjectTreeItem(Tree tree, TreeItem parent, SharpIdeProjectModel projectModel)
{
var projectItem = tree.CreateItem(parent);
var projectItem = CreateTreeItem(parent);
projectItem.SetText(0, projectModel.Name.Value);
var icon = projectModel.IsLoading ? LoadingProjectIcon : projectModel.IsInvalid ? UnloadedProjectIcon : CsprojIcon;
projectItem.SetIcon(0, icon);
Expand Down Expand Up @@ -295,7 +437,7 @@ await this.InvokeAsync(() =>
[RequiresGodotUiThread]
private TreeItem CreateFolderTreeItem(Tree tree, TreeItem parent, SharpIdeFolder sharpIdeFolder, int newStartingIndex = -1)
{
var folderItem = tree.CreateItem(parent, newStartingIndex);
var folderItem = CreateTreeItem(parent, newStartingIndex);
folderItem.SetText(0, sharpIdeFolder.Name.Value);
folderItem.SetIcon(0, FolderIcon);
folderItem.SharpIdeNode = sharpIdeFolder;
Expand Down Expand Up @@ -345,7 +487,7 @@ private TreeItem CreateFileTreeItem(Tree tree, TreeItem parent, SharpIdeFile sha
var folderCount = sharpIdeParent.Folders.Count;
newStartingIndex += folderCount;
}
var fileItem = tree.CreateItem(parent, newStartingIndex);
var fileItem = CreateTreeItem(parent, newStartingIndex);
fileItem.SetText(0, sharpIdeFile.Name.Value);
fileItem.SetIconsForFileExtension(sharpIdeFile);
if (GitColours.GetColorForGitFileStatus(sharpIdeFile.GitStatus) is { } notnullColor) fileItem.SetCustomColor(0, notnullColor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@
[ext_resource type="Texture2D" uid="uid://mnpe6bcnycfh" path="res://Features/SolutionExplorer/Resources/LoadingProjectIcon.svg" id="6_wpvwf"]
[ext_resource type="Texture2D" uid="uid://b4n5n20uey34i" path="res://Features/SolutionExplorer/Resources/UnloadedProjectIcon.svg" id="7_ykfad"]

[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ykfad"]
bg_color = Color(0.12156863, 0.6901961, 0.6431373, 0.47843137)
corner_radius_top_left = 2
corner_radius_top_right = 2
corner_radius_bottom_right = 2
corner_radius_bottom_left = 2
expand_margin_left = 1.0
expand_margin_right = 1.0

[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_idvpu"]
content_margin_left = 4.0
content_margin_top = 4.0
Expand Down Expand Up @@ -50,11 +59,28 @@ CsprojIcon = ExtResource("5_r1qfc")
LoadingProjectIcon = ExtResource("6_wpvwf")
UnloadedProjectIcon = ExtResource("7_ykfad")
SlnIcon = ExtResource("6_idvpu")
SearchMatchHighlight = SubResource("StyleBoxFlat_ykfad")

[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=1396470192]
layout_mode = 2

[node name="PanelContainer" type="PanelContainer" parent="." unique_id=1468166306]
[node name="SearchInput" type="LineEdit" parent="VBoxContainer" unique_id=724786348]
unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(0, 40)
layout_mode = 2
size_flags_horizontal = 3
theme_type_variation = &"SearchInput"
placeholder_text = "Search"
clear_button_enabled = true
select_all_on_focus = true

[node name="TreeContainer" type="PanelContainer" parent="VBoxContainer" unique_id=1468166306]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3

[node name="Tree" type="Tree" parent="PanelContainer" unique_id=1327553265]
[node name="Tree" type="Tree" parent="VBoxContainer/TreeContainer" unique_id=1327553265]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/v_separation = 1
Expand Down
3 changes: 3 additions & 0 deletions src/SharpIDE.Godot/InputStringNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public static class InputStringNames
public static readonly StringName CodeEditorDuplicateLine = nameof(CodeEditorDuplicateLine);
public static readonly StringName FindInCurrentFile = nameof(FindInCurrentFile);
public static readonly StringName ReplaceInCurrentFile = nameof(ReplaceInCurrentFile);
public static readonly StringName FindInSolutionExplorer = nameof(FindInSolutionExplorer);
}

public static class ThemeStringNames
Expand All @@ -47,6 +48,8 @@ public static class ThemeStringNames
public static readonly StringName CompletionScrollColor = "completion_scroll_color";
public static readonly StringName CompletionExistingColor = "completion_existing_color";
public static readonly StringName CompletionColorBgIcon = "completion_color_bg";

public static readonly StringName HSeparation = "h_separation";
}

public static class ThemeVariationStringNames
Expand Down
2 changes: 2 additions & 0 deletions src/SharpIDE.Godot/Resources/LightTheme.tres
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,8 @@ IdeSidebarButton/styles/normal = SubResource("StyleBoxFlat_dsk6k")
IdeSidebarButton/styles/pressed = SubResource("StyleBoxFlat_njudc")
Label/colors/font_color = Color(0.17, 0.17, 0.17, 1)
LineEdit/colors/caret_color = Color(0.05, 0.05, 0.05, 1)
LineEdit/colors/clear_button_color = Color(0.3019608, 0.3019608, 0.3019608, 1)
LineEdit/colors/clear_button_color_pressed = Color(0.3019608, 0.3019608, 0.3019608, 1)
LineEdit/colors/font_color = Color(0.12, 0.12, 0.12, 1)
LineEdit/colors/font_placeholder_color = Color(0.12, 0.12, 0.12, 0.6)
LineEdit/styles/normal = SubResource("StyleBoxFlat_guqd5")
Expand Down
5 changes: 5 additions & 0 deletions src/SharpIDE.Godot/project.godot
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ CodeEditorDuplicateLine={
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":68,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
FindInSolutionExplorer={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":70,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}

[rendering]

Expand Down