// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using System.IO; using FlaxEditor.Content; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.Assertions; using FlaxEngine.GUI; using FlaxEngine.Json; namespace FlaxEditor.Windows { public partial class ContentWindow { /// /// Occurs when content window wants to show the context menu for the given content item. Allows to add custom options. /// public event Action ContextMenuShow; private void ShowContextMenuForItem(ContentItem item, ref Float2 location, bool isTreeNode) { Assert.IsNull(_newElement); // Cache data bool isValidElement = item != null; var proxy = Editor.ContentDatabase.GetProxy(item); ContentFolder folder = null; bool isFolder = false; if (isValidElement) { isFolder = item.IsFolder; folder = isFolder ? (ContentFolder)item : item.ParentFolder; } else { folder = CurrentViewFolder; } Assert.IsNotNull(folder); bool isRootFolder = CurrentViewFolder == _root.Folder; // Create context menu ContextMenuButton b; ContextMenu cm = new ContextMenu { Tag = item }; if (isTreeNode) { b = cm.AddButton("Expand All", OnExpandAllClicked); b.Enabled = CurrentViewFolder.Node.ChildrenCount != 0; b = cm.AddButton("Collapse All", OnCollapseAllClicked); b.Enabled = CurrentViewFolder.Node.ChildrenCount != 0; cm.AddSeparator(); } if (item is ContentFolder contentFolder && contentFolder.Node is ProjectTreeNode) { cm.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(CurrentViewFolder.Path)); } else if (isValidElement) { b = cm.AddButton("Open", () => Open(item)); b.Enabled = proxy != null || isFolder; if (_view.SelectedCount > 1) b = cm.AddButton("Open (all selected)", () => { foreach (var e in _view.Selection) Open(e); }); cm.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(System.IO.Path.GetDirectoryName(item.Path))); if (item.HasDefaultThumbnail == false) { cm.AddButton("Refresh thumbnail", item.RefreshThumbnail); } if (!isFolder) { b = cm.AddButton("Reimport", ReimportSelection); b.Enabled = proxy != null && proxy.CanReimport(item); if (item is BinaryAssetItem binaryAsset) { if (!binaryAsset.GetImportPath(out string importPath)) { string importLocation = System.IO.Path.GetDirectoryName(importPath); if (!string.IsNullOrEmpty(importLocation) && System.IO.Directory.Exists(importLocation)) { cm.AddButton("Show import location", () => FileSystem.ShowFileExplorer(importLocation)); } } } if (item is AssetItem assetItem) { if (assetItem.IsLoaded) cm.AddButton("Reload", assetItem.Reload); cm.AddButton("Copy asset ID", () => Clipboard.Text = JsonSerializer.GetStringID(assetItem.ID)); cm.AddButton("Select actors using this asset", () => Editor.SceneEditing.SelectActorsUsingAsset(assetItem.ID)); cm.AddButton("Show asset references graph", () => Editor.Windows.Open(new AssetReferencesGraphWindow(Editor, assetItem))); } if (Editor.CanExport(item.Path)) { b = cm.AddButton("Export", ExportSelection); } } cm.AddButton("Delete", () => Delete(item)); cm.AddSeparator(); cm.AddButton("Duplicate", _view.Duplicate); cm.AddButton("Copy", _view.Copy); b = cm.AddButton("Paste", _view.Paste); b.Enabled = _view.CanPaste(); cm.AddButton("Rename", () => Rename(item)); // Custom options ContextMenuShow?.Invoke(cm, item); proxy?.OnContentWindowContextMenu(cm, item); item.OnContextMenu(cm); cm.AddButton("Copy name to Clipboard", () => Clipboard.Text = item.NamePath); cm.AddButton("Copy path to Clipboard", () => Clipboard.Text = item.Path); } else { cm.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(CurrentViewFolder.Path)); b = cm.AddButton("Paste", _view.Paste); b.Enabled = _view.CanPaste(); cm.AddButton("Refresh", () => Editor.ContentDatabase.RefreshFolder(CurrentViewFolder, true)); cm.AddButton("Refresh all thumbnails", RefreshViewItemsThumbnails); } cm.AddSeparator(); // Check if is source folder to add new module if (folder?.ParentFolder?.Node is ProjectTreeNode parentFolderNode && folder.Node == parentFolderNode.Source) { var button = cm.AddButton("New module"); button.CloseMenuOnClick = false; button.Clicked += () => NewModule(button, parentFolderNode.Source.Path); } if (!isRootFolder && !(item is ContentFolder projectFolder && projectFolder.Node is ProjectTreeNode)) { cm.AddButton("New folder", NewFolder); } // Loop through each proxy and user defined json type and add them to the context menu var actorType = new ScriptType(typeof(Actor)); var scriptType = new ScriptType(typeof(Script)); foreach (var type in Editor.CodeEditing.All.Get()) { if (type.IsAbstract || type.Type == null) continue; if (actorType.IsAssignableFrom(type) || scriptType.IsAssignableFrom(type)) continue; // Get attribute ContentContextMenuAttribute attribute = null; foreach (var typeAttribute in type.GetAttributes(false)) { if (typeAttribute is ContentContextMenuAttribute contentContextMenuAttribute) { attribute = contentContextMenuAttribute; break; } } if (attribute == null) continue; // Get context proxy ContentProxy p; if (type.Type.IsSubclassOf(typeof(ContentProxy))) { p = Editor.ContentDatabase.Proxy.Find(x => x.GetType() == type.Type); } else { // User can use attribute to put their own assets into the content context menu var generic = typeof(SpawnableJsonAssetProxy<>).MakeGenericType(type.Type); var instance = Activator.CreateInstance(generic); p = instance as AssetProxy; } if (p == null) continue; if (p.CanCreate(folder)) { var parts = attribute.Path.Split('/'); ContextMenuChildMenu childCM = null; bool mainCM = true; for (int i = 0; i < parts?.Length; i++) { var part = parts[i].Trim(); if (i == parts.Length - 1) { if (mainCM) { cm.AddButton(part, () => NewItem(p)); mainCM = false; } else if (childCM != null) { childCM.ContextMenu.AddButton(part, () => NewItem(p)); childCM.ContextMenu.AutoSort = true; } } else { if (mainCM) { childCM = cm.GetOrAddChildMenu(part); childCM.ContextMenu.AutoSort = true; mainCM = false; } else if (childCM != null) { childCM = childCM.ContextMenu.GetOrAddChildMenu(part); childCM.ContextMenu.AutoSort = true; } } } } } if (folder.CanHaveAssets) { cm.AddButton("Import file", () => { _view.ClearSelection(); Editor.ContentImporting.ShowImportFileDialog(CurrentViewFolder); }); } // Show it cm.Show(this, location); } private void OnExpandAllClicked(ContextMenuButton button) { CurrentViewFolder.Node.ExpandAll(); } private void OnCollapseAllClicked(ContextMenuButton button) { CurrentViewFolder.Node.CollapseAll(); } /// /// Refreshes thumbnails for all the items in the view. /// private void RefreshViewItemsThumbnails() { var items = _view.Items; for (int i = 0; i < items.Count; i++) { items[i].RefreshThumbnail(); } } /// /// Reimports the selected assets. /// private void ReimportSelection() { var selection = _view.Selection; for (int i = 0; i < selection.Count; i++) { if (selection[i] is BinaryAssetItem binaryAssetItem) Editor.ContentImporting.Reimport(binaryAssetItem); } } private bool Export(ContentItem item, string outputFolder) { if (item is ContentFolder folder) { for (int i = 0; i < folder.Children.Count; i++) { if (Export(folder.Children[i], outputFolder)) return true; } } else if (item is AssetItem asset && Editor.CanExport(asset.Path)) { if (Editor.Export(asset.Path, outputFolder)) return true; } return false; } /// /// Exports the selected items. /// private void ExportSelection() { if (FileSystem.ShowBrowseFolderDialog(Editor.Windows.MainWindow, null, "Select the output folder", out var outputFolder)) return; var selection = _view.Selection; for (int i = 0; i < selection.Count; i++) { if (Export(selection[i], outputFolder)) return; } } private void NewModule(ContextMenuButton button, string path) { var popup = new ContextMenuBase { Size = new Float2(230, 125), ClipChildren = false, CullChildren = false, }; popup.Show(button, new Float2(button.Width, 0)); var nameLabel = new Label { Parent = popup, AnchorPreset = AnchorPresets.TopLeft, Text = "Name", HorizontalAlignment = TextAlignment.Near, }; nameLabel.LocalX += 10; nameLabel.LocalY += 10; var nameTextBox = new TextBox { Parent = popup, WatermarkText = "Module Name", AnchorPreset = AnchorPresets.TopLeft, IsMultiline = false, }; nameTextBox.LocalX += 100; nameTextBox.LocalY += 10; var defaultTextBoxBorderColor = nameTextBox.BorderColor; var defaultTextBoxBorderSelectedColor = nameTextBox.BorderSelectedColor; nameTextBox.TextChanged += () => { if (string.IsNullOrEmpty(nameTextBox.Text)) { nameTextBox.BorderColor = defaultTextBoxBorderColor; nameTextBox.BorderSelectedColor = defaultTextBoxBorderSelectedColor; return; } var pluginPath = Path.Combine(Globals.ProjectFolder, "Source", nameTextBox.Text); if (Directory.Exists(pluginPath)) { nameTextBox.BorderColor = Color.Red; nameTextBox.BorderSelectedColor = Color.Red; } else { nameTextBox.BorderColor = defaultTextBoxBorderColor; nameTextBox.BorderSelectedColor = defaultTextBoxBorderSelectedColor; } }; var editorLabel = new Label { Parent = popup, AnchorPreset = AnchorPresets.TopLeft, Text = "Editor", HorizontalAlignment = TextAlignment.Near, }; editorLabel.LocalX += 10; editorLabel.LocalY += 35; var editorCheckBox = new CheckBox { Parent = popup, AnchorPreset = AnchorPresets.TopLeft, }; editorCheckBox.LocalY += 35; editorCheckBox.LocalX += 100; var cppLabel = new Label { Parent = popup, AnchorPreset = AnchorPresets.TopLeft, Text = "C++", HorizontalAlignment = TextAlignment.Near, }; cppLabel.LocalX += 10; cppLabel.LocalY += 60; var cppCheckBox = new CheckBox { Parent = popup, AnchorPreset = AnchorPresets.TopLeft, }; cppCheckBox.LocalY += 60; cppCheckBox.LocalX += 100; var submitButton = new Button { Parent = popup, AnchorPreset = AnchorPresets.TopLeft, Text = "Create", Width = 70, }; submitButton.LocalX += 40; submitButton.LocalY += 90; submitButton.Clicked += () => { // TODO: Check all modules in project including plugins if (Directory.Exists(Path.Combine(Globals.ProjectFolder, "Source", nameTextBox.Text))) { Editor.LogWarning("Cannot create module due to name conflict."); return; } Editor.CodeEditing.CreateModule(path, nameTextBox.Text, editorCheckBox.Checked, cppCheckBox.Checked); nameTextBox.Clear(); editorCheckBox.Checked = false; cppCheckBox.Checked = false; popup.Hide(); button.ParentContextMenu.Hide(); }; var cancelButton = new Button { Parent = popup, AnchorPreset = AnchorPresets.TopLeft, Text = "Cancel", Width = 70, }; cancelButton.LocalX += 120; cancelButton.LocalY += 90; cancelButton.Clicked += () => { nameTextBox.Clear(); editorCheckBox.Checked = false; cppCheckBox.Checked = false; popup.Hide(); button.ParentContextMenu.Hide(); }; } } }