diff --git a/FrostyEditor/Windows/MainWindow.xaml.cs b/FrostyEditor/Windows/MainWindow.xaml.cs index eb1ba392e..20ac482de 100644 --- a/FrostyEditor/Windows/MainWindow.xaml.cs +++ b/FrostyEditor/Windows/MainWindow.xaml.cs @@ -522,8 +522,7 @@ private void NewProject() // clear all modifications App.AssetManager.Reset(); - dataExplorer.RefreshAll(); - legacyExplorer.RefreshAll(); + UpdateUI(true); // create a new blank project m_project = new FrostyProject(); @@ -644,7 +643,7 @@ private void LoadProject(string filename, bool saveProject) m_project = newProject; - UpdateUI(); + UpdateUI(true); legacyExplorer.ShowOnlyModified = false; legacyExplorer.ShowOnlyModified = true; @@ -967,8 +966,8 @@ private void contextMenuRevert_Click(object sender, RoutedEventArgs e) FrostyTaskWindow.Show("Reverting Asset", "", (task) => { App.AssetManager.RevertAsset(entry, suppressOnModify: false); }); - dataExplorer.RefreshAll(); - legacyExplorer.RefreshAll(); + dataExplorer.RefreshAll(true); + legacyExplorer.RefreshAll(true); } private void contextMenuImportAsset_Click(object sender, RoutedEventArgs e) diff --git a/FrostyPlugin/Controls/FrostyDataExplorer.cs b/FrostyPlugin/Controls/FrostyDataExplorer.cs index b19e7dd14..6993cdd0f 100644 --- a/FrostyPlugin/Controls/FrostyDataExplorer.cs +++ b/FrostyPlugin/Controls/FrostyDataExplorer.cs @@ -13,6 +13,7 @@ using Frosty.Core.Commands; using System.Windows.Data; using FrostySdk.Managers.Entries; +using System.Runtime.CompilerServices; namespace Frosty.Core.Controls { @@ -58,7 +59,7 @@ public double ItemHeight //} } - internal class AssetPath + public class AssetPath : INotifyPropertyChanged { private static readonly ImageSource ClosedImage = new ImageSourceConverter().ConvertFromString("pack://application:,,,/FrostyEditor;component/Images/CloseFolder.png") as ImageSource; private static readonly ImageSource OpenImage = new ImageSourceConverter().ConvertFromString("pack://application:,,,/FrostyEditor;component/Images/OpenFolder.png") as ImageSource; @@ -67,17 +68,38 @@ internal class AssetPath public string PathName { get; private set; } public string FullPath { get; } public AssetPath Parent { get; } - public List Children { get; } = new List(); - public bool IsSelected { get; set; } + public List Children { get; private set; } = new List(); + public bool IsSelected { + get => selected; + set { + selected = value; + OnPropertyChanged(); + } + } + private bool selected; public bool IsRoot { get; } public bool IsExpanded { get => expanded && Children.Count != 0; - set => expanded = value; + set { + expanded = value; + OnPropertyChanged(); + } } private bool expanded; + public List Entries { get; set; } = new List(); + + public AssetPath Clone; + + public event PropertyChangedEventHandler PropertyChanged; + + protected void OnPropertyChanged([CallerMemberName] string name = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + } + public AssetPath(string inName, string path, AssetPath inParent, bool bInRoot = false) { PathName = inName; @@ -86,10 +108,81 @@ public AssetPath(string inName, string path, AssetPath inParent, bool bInRoot = Parent = inParent; } + public AssetPath Copy(bool keepVisualState = false) + { + AssetPath path = (AssetPath)MemberwiseClone(); + path.Children = new List(); + path.Entries = new List(); + + if (keepVisualState && Clone != null) + { + // The base object will never have expanded/selected set to true. + // The clone object is displayed on the UI & bound to the treeview + // so if we keep visual state, we need to take it from the previous clone. + path.IsExpanded = Clone.IsExpanded; + path.IsSelected = Clone.IsSelected; + } + + Clone = path; + + return path; + } + public void UpdatePathName(string newName) { PathName = newName; } + + public AssetPath Search(Func entryFilterFunction, bool keepVisualState = false) + { + // Recursively search the Children and Entries of each node. + // Make a copy of the node so that the original data is never modified. + // The copied nodes are then displayed on the UI. + List filteredChildNodes = null; + List filteredEntries = null; + + + // If the node has children, recursively call each child to filter + if (Children != null && Children.Count > 0) + { + filteredChildNodes = Children + .Select(i => i.Search(entryFilterFunction, keepVisualState)) + .Where(i => i != null).ToList(); + } + + if (Entries != null && Entries.Count > 0) + { + filteredEntries = Entries.Where(entry => entryFilterFunction(entry)).ToList(); + } + + // If any of this node's entries pass the filter, or if any child of this node's entries pass the filter, + // then this node must pass the filter. Make a copy of this node and add all children & entries that passed the filter + bool hasChildNodes = filteredChildNodes != null && filteredChildNodes.Any(); + bool hasEntries = filteredEntries != null && filteredEntries.Any(); + + if (hasChildNodes || hasEntries) + { + AssetPath result = Copy(keepVisualState); + + if (hasChildNodes) + result.Children.AddRange(filteredChildNodes); + + if (hasEntries) + result.Entries.AddRange(filteredEntries); + + return result; + } + else + { + // If nothing passes the filter, return null to ensure it doesn't display + return null; + } + } + + public int GetEntryCount() + { + return Entries.Count + Children.Aggregate(0, (acc, i) => acc + i.GetEntryCount()); + } } public class AssetDoubleClickedEventArgs : RoutedEventArgs @@ -110,6 +203,8 @@ public class FrostyDataExplorer : Control { public string FilteredText { get => m_filterTextBox.Text; } + public Func EntryFilterFunction { get; set; } = null; + private const string PART_ShowOnlyModifiedCheckBox = "PART_ShowOnlyModifiedCheckBox"; private const string PART_FilterTextBox = "PART_FilterTextBox"; private const string PART_AssetTreeView = "PART_AssetTreeView"; @@ -154,7 +249,7 @@ private static void OnItemsSourceChanged(DependencyObject o, DependencyPropertyC FrostyDataExplorer ctrl = o as FrostyDataExplorer; ctrl.m_assetPathMapping.Clear(); ctrl.SelectedAsset = null; - ctrl.UpdateTreeView(); + ctrl.CreateAssetPathMapping(); } #endregion @@ -391,7 +486,11 @@ public override void OnApplyTemplate() if (MultiSelect) m_assetListView.SelectionMode = SelectionMode.Extended; - UpdateTreeView(); + if (m_assetPathMapping.Count == 0 && ItemsSource != null) + { + CreateAssetPathMapping(); + UpdateTreeView(); + } if (IsVisible && m_bookmarkContext != null) { @@ -468,8 +567,8 @@ public void SelectAsset(AssetEntry entry) } tvi = (tvi == null) - ? (TreeViewItem)m_assetTreeView.ItemContainerGenerator.ContainerFromItem(path) - : (TreeViewItem)tvi.ItemContainerGenerator.ContainerFromItem(path); + ? (TreeViewItem)m_assetTreeView.ItemContainerGenerator.ContainerFromItem(path?.Clone) + : (TreeViewItem)tvi.ItemContainerGenerator.ContainerFromItem(path?.Clone); } if (tvi != null) { @@ -479,8 +578,8 @@ public void SelectAsset(AssetEntry entry) } else { - TreeViewItem tvi = m_assetTreeView.ItemContainerGenerator.ContainerFromItem(selectedPath) as TreeViewItem; - if(tvi != null) + TreeViewItem tvi = m_assetTreeView.ItemContainerGenerator.ContainerFromItem(selectedPath?.Clone) as TreeViewItem; + if (tvi != null) { tvi.BringIntoView(); tvi.IsSelected = true; @@ -508,9 +607,17 @@ public void RefreshItems() } - public void RefreshAll() + public void RefreshAll(bool keepVisualState = false) { - UpdateTreeView(); + UpdateTreeView(keepVisualState); + + if (SelectedAssets != null) + { + foreach (var item in SelectedAssets) + { + m_assetListView.SelectedItems.Add(item); + } + } } public void FocusFilter() @@ -521,7 +628,7 @@ public void FocusFilter() private void assetTreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs e) { m_selectedPath = m_assetTreeView.SelectedItem as AssetPath; - SelectedPath = string.IsNullOrEmpty(m_selectedPath.FullPath) ? "" : m_selectedPath.FullPath.Remove(0, 1); + SelectedPath = string.IsNullOrEmpty(m_selectedPath?.FullPath) ? "" : m_selectedPath.FullPath.Remove(0, 1); UpdateListView(m_selectedPath); } @@ -579,78 +686,76 @@ private void assetListViewColumn_Click(object sender, RoutedEventArgs e) m_lastSortDirection = sortDir; } - private void UpdateTreeView() + private void CreateAssetPathMapping() { - if (m_assetTreeView == null) - return; + if (m_assetTreeView == null) return; - if (m_selectedPath != null) - m_selectedPath.IsSelected = false; + AssetPath parent = new AssetPath("", "", null); + AssetPath root = new AssetPath("![root]", "", null, true); + parent.Children.Add(root); - if (ItemsSource == null) - return; + m_assetPathMapping.Add("__root__", parent); + m_assetPathMapping.Add("/", root); - AssetPath root = new AssetPath("", "", null); foreach (AssetEntry entry in ItemsSource) { - if (ShowOnlyModified && !entry.IsModified) - continue; - - if (!FilterText(entry.Name, entry)) - continue; + if (EntryFilterFunction != null && !EntryFilterFunction(entry)) { continue; } string[] arr = entry.Path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); - AssetPath next = root; + AssetPath next = arr.Length > 0 ? parent : root; foreach (string path in arr) { - bool bFound = false; - foreach (AssetPath child in next.Children) - { - if (child.PathName.Equals(path, StringComparison.OrdinalIgnoreCase)) - { - if (path.ToCharArray().Any(char.IsUpper)) - child.UpdatePathName(path); + string fullPath = next.FullPath + "/" + path; + AssetPath newPath = null; - next = child; - bFound = true; - break; - } + if (!m_assetPathMapping.ContainsKey(fullPath)) + { + newPath = new AssetPath(path, fullPath, next); + m_assetPathMapping.Add(fullPath, newPath); + next.Children.Add(newPath); } - - if (!bFound) + else { - string fullPath = next.FullPath + "/" + path; - AssetPath newPath = null; + newPath = m_assetPathMapping[fullPath]; - if (!m_assetPathMapping.ContainsKey(fullPath)) - { - newPath = new AssetPath(path, fullPath, next); - m_assetPathMapping.Add(fullPath, newPath); - } - else - { - newPath = m_assetPathMapping[fullPath]; - newPath.Children.Clear(); - - if (newPath == m_selectedPath) - m_selectedPath.IsSelected = true; - } - - next.Children.Add(newPath); - next = newPath; + if (newPath == m_selectedPath) + m_selectedPath.IsSelected = true; } + + next = newPath; } - } - if(!m_assetPathMapping.ContainsKey("/")) - m_assetPathMapping.Add("/", new AssetPath("![root]", "", null, true)); - root.Children.Insert(0, m_assetPathMapping["/"]); + next.Entries.Add(entry); + } - m_assetTreeView.ItemsSource = root.Children; + m_assetTreeView.ItemsSource = parent.Children; m_assetTreeView.Items.SortDescriptions.Add(new SortDescription("PathName", ListSortDirection.Ascending)); + } - UpdateListView(m_selectedPath); + private bool ShouldDisplayAsset(AssetEntry entry) + { + return entry != null && + (!ShowOnlyModified || entry.IsModified) && FilterText(entry.Name, entry); + } + + private void UpdateTreeView(bool keepVisualState = true) + { + if (m_assetTreeView == null) + return; + + if (!keepVisualState && m_selectedPath != null) + m_selectedPath.IsSelected = false; + + if (ItemsSource == null) + return; + + AssetPath root = m_assetPathMapping["__root__"]; + AssetPath newRoot = root.Search(ShouldDisplayAsset, keepVisualState); + + m_assetTreeView.SelectedValuePath = String.Empty; + m_assetTreeView.ItemsSource = newRoot?.Children; + m_assetTreeView.Items.Refresh(); } private void UpdateListView(AssetPath path = null) @@ -661,27 +766,19 @@ private void UpdateListView(AssetPath path = null) return; } - List items = new List(); - string fullPath = path.FullPath.Trim('/'); - - foreach (AssetEntry entry in ItemsSource) - { - if (ShowOnlyModified && !entry.IsModified) - continue; - if (entry.Path.Equals(fullPath, StringComparison.OrdinalIgnoreCase)) - { - if (!FilterText(entry.Name, entry)) - continue; - items.Add(entry); - } - } - - m_assetListView.ItemsSource = items; + m_assetListView.ItemsSource = path.Entries; if (SelectedAsset != null) { m_assetListView.SelectedItem = SelectedAsset; } + else if (SelectedAssets != null) + { + foreach (var item in SelectedAssets) + { + m_assetListView.SelectedItems.Add(item); + } + } } private void assetListView_SelectionChanged(object sender, SelectionChangedEventArgs e) diff --git a/FrostyPlugin/Controls/FrostyHandledExceptionBox.cs b/FrostyPlugin/Controls/FrostyHandledExceptionBox.cs index d2dbd0f4c..ffcca0074 100644 --- a/FrostyPlugin/Controls/FrostyHandledExceptionBox.cs +++ b/FrostyPlugin/Controls/FrostyHandledExceptionBox.cs @@ -45,4 +45,4 @@ public static MessageBoxResult Show(string warning) return (window.ShowDialog() == true) ? MessageBoxResult.OK : MessageBoxResult.Cancel; } } -} \ No newline at end of file +} diff --git a/FrostyPlugin/Themes/Generic.xaml b/FrostyPlugin/Themes/Generic.xaml index fcc4eab6a..45efefd27 100644 --- a/FrostyPlugin/Themes/Generic.xaml +++ b/FrostyPlugin/Themes/Generic.xaml @@ -238,7 +238,7 @@ - + @@ -1224,7 +1224,7 @@ - + + + + + + @@ -1433,15 +1438,12 @@ - - -