diff --git a/Fushigi/ui/CourseAreaEditContext.cs b/Fushigi/ui/CourseAreaEditContext.cs index 2d7379ba..06c5f65b 100644 --- a/Fushigi/ui/CourseAreaEditContext.cs +++ b/Fushigi/ui/CourseAreaEditContext.cs @@ -35,20 +35,6 @@ public void DeleteActor(CourseActor actor) batchAction.Commit($"{IconUtil.ICON_TRASH} Delete {actor.mPackName}"); } - public void DeleteSelectedActors() - { - var selectedActors = GetSelectedObjects().ToList(); - - var batchAction = BeginBatchAction(); - - foreach (var actor in selectedActors) - { - DeleteActor(actor); - } - - batchAction.Commit($"{IconUtil.ICON_TRASH} Delete selected"); - } - public void AddActorToGroup(CourseGroup group, CourseActor actor) { Console.WriteLine($"Adding actor {actor.mPackName}[{actor.mHash}] to group [{group.mHash}]."); diff --git a/Fushigi/ui/MainWindow.cs b/Fushigi/ui/MainWindow.cs index cdd10152..d0e02acb 100644 --- a/Fushigi/ui/MainWindow.cs +++ b/Fushigi/ui/MainWindow.cs @@ -153,7 +153,7 @@ void LoadFromSettings(GL gl) if (latestCourse != null && ParamDB.sIsInit) { mCurrentCourseName = latestCourse; - mSelectedCourseScene = new(new(mCurrentCourseName), gl); + mSelectedCourseScene = new(new(mCurrentCourseName), gl, this); mIsChoosingPreferences = false; mIsWelcome = false; } @@ -194,7 +194,7 @@ void DrawMainMenu(GL gl) { mCurrentCourseName = selectedCourse; Console.WriteLine($"Selected course {mCurrentCourseName}!"); - mSelectedCourseScene = new(new(mCurrentCourseName), gl); + mSelectedCourseScene = new(new(mCurrentCourseName), gl, this); UserSettings.AppendRecentCourse(mCurrentCourseName); } }).ConfigureAwait(false); //fire and forget diff --git a/Fushigi/ui/SceneObjects/bgunit/BGUnitRailSceneObj.cs b/Fushigi/ui/SceneObjects/bgunit/BGUnitRailSceneObj.cs index 22e46301..8db0c206 100644 --- a/Fushigi/ui/SceneObjects/bgunit/BGUnitRailSceneObj.cs +++ b/Fushigi/ui/SceneObjects/bgunit/BGUnitRailSceneObj.cs @@ -301,8 +301,9 @@ void IViewportDrawable.Draw2D(CourseAreaEditContext ctx, LevelViewport viewport, if (ImGui.IsMouseReleased(0)) OnMouseUp(ctx, viewport); - if (viewport.mEditorState == LevelViewport.EditorState.Selecting) - OnSelecting(ctx, viewport); + //TODO does it still need a condition like this? + //if (viewport.mEditorState == LevelViewport.EditorState.Selecting) + OnSelecting(ctx, viewport); OnKeyDown(ctx, viewport); diff --git a/Fushigi/ui/widgets/CourseScene.cs b/Fushigi/ui/widgets/CourseScene.cs index 15e20b8d..febd09f2 100644 --- a/Fushigi/ui/widgets/CourseScene.cs +++ b/Fushigi/ui/widgets/CourseScene.cs @@ -2,6 +2,7 @@ using Fushigi.gl; using Fushigi.gl.Bfres; using Fushigi.param; +using Fushigi.ui.modal; using Fushigi.ui.SceneObjects; using Fushigi.ui.SceneObjects.bgunit; using Fushigi.ui.undo; @@ -33,19 +34,17 @@ class CourseScene PropertyDictCapture.Empty); readonly Course course; + readonly IPopupModalHost mPopupModalHost; CourseArea selectedArea; readonly Dictionary mLayersVisibility = []; bool mHasFilledLayers = false; bool mAllLayersVisible = true; - bool mShowAddActor = false; - bool mShowSelectActorLayer = false; + readonly List mOpenToolWindows = []; string mActorSearchText = ""; CourseLink? mSelectedGlobalLink = null; - string mAddActorSearchQuery = ""; - string mAddLayerSearchQuery = ""; string[] linkTypes = [ "BasicSignal", @@ -76,9 +75,10 @@ class CourseScene "EventGuest_11", ]; - public CourseScene(Course course, GL gl) + public CourseScene(Course course, GL gl, IPopupModalHost popupModalHost) { this.course = course; + this.mPopupModalHost = popupModalHost; selectedArea = course.GetArea(0); undoWindow = new UndoWindow(); @@ -86,8 +86,17 @@ public CourseScene(Course course, GL gl) { var areaScene = new CourseAreaScene(area, new CourseAreaSceneRoot(area)); areaScenes[area] = areaScene; - viewports[area] = new LevelViewport(area, gl, areaScene); + var viewport = new LevelViewport(area, gl, areaScene); + viewports[area] = viewport; lastSavedAction[area] = null; + + //might not be the best approach but better than what we had before + viewport.ObjectDeletionRequested += (objs) => + { + if (objs.Count > 0) + _ = DeleteObjectsWithWarningPrompt(objs, + areaScene.EditContext, "Delete objects"); + }; } activeViewport = viewports[selectedArea]; @@ -180,20 +189,18 @@ public void DrawUI(GL gl, double deltaSeconds) BGUnitPanel(); CourseMiniView(); - - if (mShowAddActor) + + for (int i = 0; i < mOpenToolWindows.Count; i++) { - SelectActorToAdd(); - } + var window = mOpenToolWindows[i]; + bool windowOpen = true; + window.Draw(ref windowOpen); - if (mShowSelectActorLayer) - { - SelectActorToAddLayer(); - } - - if (activeViewport.mEditorState == LevelViewport.EditorState.DeleteActorLinkCheck) - { - LinkDeletionCheck(); + if (!windowOpen) + { + mOpenToolWindows.RemoveAt(i); + i--; + } } @@ -285,231 +292,25 @@ void UndoHistoryPanel() undoWindow.Render(areaScenes[selectedArea].EditContext); } - private void SelectActorToAdd() - { - bool button = true; - bool status = ImGui.Begin("Add Actor", ref button); - - ImGui.InputText("Search", ref mAddActorSearchQuery, 256); - - var filteredActors = ParamDB.GetActors().ToImmutableList(); - - if (mAddActorSearchQuery != "") - { - filteredActors = FuzzySharp.Process.ExtractAll(mAddActorSearchQuery, ParamDB.GetActors(), cutoff: 65) - .OrderByDescending(result => result.Score) - .Select(result => result.Value) - .ToImmutableList(); - } - - if (ImGui.BeginListBox("Select the actor you want to add.", ImGui.GetContentRegionAvail())) - { - foreach (string actor in filteredActors) - { - ImGui.Selectable(actor); - - if (ImGui.IsItemHovered() && ImGui.IsMouseDoubleClicked(0)) - { - Console.WriteLine("Switching state to EditorState.SelectingActorLayer"); - activeViewport.mEditorState = LevelViewport.EditorState.SelectingActorLayer; - activeViewport.mActorToAdd = actor; - mShowAddActor = false; - mShowSelectActorLayer = true; - } - } - - ImGui.EndListBox(); - } - - if (ImGui.IsKeyDown(ImGuiKey.Escape)) - { - button = false; - } - - if (!button) - { - Console.WriteLine("Switching state to EditorState.Selecting"); - activeViewport.mEditorState = LevelViewport.EditorState.Selecting; - mShowAddActor = false; - } - - if (status) - { - ImGui.End(); - } - } - - private void SelectActorToAddLayer() - { - bool button = true; - bool status = ImGui.Begin("Select Layer", ref button); - - ImGui.InputText("Search", ref mAddLayerSearchQuery, 256); - - var fileteredLayers = mLayersVisibility.Keys.ToArray().ToImmutableList(); - - if (mAddLayerSearchQuery != "") - { - fileteredLayers = FuzzySharp.Process.ExtractAll(mAddLayerSearchQuery, mLayersVisibility.Keys.ToArray(), cutoff: 65) - .OrderByDescending(result => result.Score) - .Select(result => result.Value) - .ToImmutableList(); - } - - if (ImGui.BeginListBox("Select the layer you want to add the actor to.", ImGui.GetContentRegionAvail())) - { - foreach (string layer in fileteredLayers) - { - ImGui.Selectable(layer); - - if (ImGui.IsItemHovered() && ImGui.IsMouseDoubleClicked(0)) - { - Console.WriteLine("Switching state to EditorState.AddingActor"); - activeViewport.mEditorState = LevelViewport.EditorState.AddingActor; - activeViewport.mLayerToAdd = layer; - mShowSelectActorLayer = false; - } - } - - ImGui.EndListBox(); - } - - if (ImGui.IsKeyDown(ImGuiKey.Escape)) - { - button = false; - } - - if (!button) - { - Console.WriteLine("Switching state to EditorState.Selecting"); - activeViewport.mEditorState = LevelViewport.EditorState.Selecting; - mShowAddActor = false; - } - - if (status) - { - ImGui.End(); - } - } - - private void LinkDeletionCheck() - { - var editContext = areaScenes[selectedArea].EditContext; - - var actors = editContext.GetSelectedObjects(); - List dstMsgStrs = new(); - List srcMsgStr = new(); - - foreach (var actor in actors) - { - if (selectedArea.mLinkHolder.HasLinksWithDest(actor.mHash)) - { - var links = selectedArea.mLinkHolder.GetSrcHashesFromDest(actor.mHash); - - foreach (KeyValuePair> kvp in links) - { - var hashes = kvp.Value; - - foreach (var hash in hashes) - { - /* only delete actors that the hash exists for...this may be caused by a user already deleting the source actor */ - if (selectedArea.mActorHolder.TryGetActor(hash, out _)) - { - dstMsgStrs.Add($"{selectedArea.mActorHolder[hash].mPackName} [{selectedArea.mActorHolder[hash].mName}]\n"); - } - } - } - - var destHashes = selectedArea.mLinkHolder.GetDestHashesFromSrc(actor.mHash); - - foreach (KeyValuePair> kvp in destHashes) - { - var hashes = kvp.Value; - - foreach (var hash in hashes) - { - if (selectedArea.mActorHolder.TryGetActor(hash, out _)) - { - srcMsgStr.Add($"{selectedArea.mActorHolder[hash].mPackName} [{selectedArea.mActorHolder[hash].mName}]\n"); - } - } - } - } - } - - /* nothing to worry about here */ - if (dstMsgStrs.Count == 0 && srcMsgStr.Count == 0) - { - var ctx = areaScenes[selectedArea].EditContext; - - if (ctx.IsAnySelected()) - { - ctx.DeleteSelectedActors(); - } - Console.WriteLine("Switching state to EditorState.Selecting"); - activeViewport.mEditorState = LevelViewport.EditorState.Selecting; - return; - } - - bool status = ImGui.Begin("Link Warning"); - - if (srcMsgStr.Count > 0) - { - ImGui.Text("The actor you are about to delete is a source link for the following actors:"); - - foreach (string s in srcMsgStr) - { - ImGui.Text(s); - } - } - - if (dstMsgStrs.Count > 0) - { - ImGui.Text("The actor you are about to delete is a destination link for the following actors:"); - - foreach (string s in dstMsgStrs) - { - ImGui.Text(s); - } - } - - ImGui.Text(" Do you wish to continue?"); - - if (ImGui.Button("Yes")) - { - Console.WriteLine("Switching state to EditorState.Selecting"); - editContext.DeleteSelectedActors(); - activeViewport.mEditorState = LevelViewport.EditorState.Selecting; - } - - ImGui.SameLine(); - - if (ImGui.Button("No")) - { - Console.WriteLine("Switching state to EditorState.Selecting"); - activeViewport.mEditorState = LevelViewport.EditorState.Selecting; - } - - if (status) - { - ImGui.End(); - } - } - private void ActorsPanel() { ImGui.Begin("Actors"); if (ImGui.Button("Add Actor")) { - mShowAddActor = true; + _ = AddActorsWithSelectActorAndLayerWindow(); } ImGui.SameLine(); if (ImGui.Button("Delete Actor")) { - activeViewport.mEditorState = LevelViewport.EditorState.DeleteActorLinkCheck; + var ctx = areaScenes[selectedArea].EditContext; + var actors = ctx.GetSelectedObjects().ToList(); + + if (actors.Count > 0) + _ = DeleteObjectsWithWarningPrompt(actors, + ctx, "Delete actors"); } ImGui.AlignTextToFramePadding(); @@ -776,10 +577,20 @@ private void SelectionParameterPanel() if (ImGui.Selectable(linkType)) { - activeViewport.mNewLinkType = linkType; - activeViewport.mIsLinkNew = true; - activeViewport.mEditorState = LevelViewport.EditorState.SelectingLinkDest; ImGui.SetWindowFocus(selectedArea.GetName()); + Task.Run(async () => + { + var pickedDest = await PickLinkDestInViewportFor(mSelectedActor); + if (pickedDest is null) + return; + + var link = new CourseLink(linkType) + { + mSource = mSelectedActor.mHash, + mDest = pickedDest.mHash + }; + editContext.AddLink(link); + }); } } @@ -847,9 +658,21 @@ private void SelectionParameterPanel() ImGui.SetCursorPos(cursor); if (ImGui.Button(IconUtil.ICON_EYE_DROPPER)) { - activeViewport.mEditorState = LevelViewport.EditorState.SelectingLinkDest; - activeViewport.CurCourseLink = selectedArea.mLinkHolder.GetLinkWithDestHash(hashArray[i]); ImGui.SetWindowFocus(selectedArea.GetName()); + Task.Run(async () => + { + var pickedDest = await PickLinkDestInViewportFor(mSelectedActor); + if (pickedDest is null) + return; + + //TODO rework GetDestHashesFromSrc to return the actual link objects or do it in another way + var link = selectedArea.mLinkHolder.mLinks.Find( + x => x.mSource == mSelectedActor.mHash && + x.mLinkName == linkName && + x.mDest == destActor!.mHash); + + link.mDest = pickedDest.mHash; + }); } ImGui.PopClipRect(); @@ -896,7 +719,6 @@ private void SelectionParameterPanel() } if(needsRecapture || propertyCapture.courseObj != mSelectedActor) { - Console.WriteLine("Capturing"); propertyCapture = ( mSelectedActor, new PropertyFieldsCapture(mSelectedActor), @@ -2045,5 +1867,233 @@ public Course GetCourse() { return course; } + + private async Task PickLinkDestInViewportFor(CourseActor source) + { + var (picked, _) = await activeViewport.PickObject( + "Select the destination actor you wish to link to.", + x => x is CourseActor && x != source); + return picked as CourseActor; + } + + private async Task DeleteObjectsWithWarningPrompt(IReadOnlyList objectsToDelete, + CourseAreaEditContext ctx, string actionName) + { + var actors = objectsToDelete.OfType(); + List dstMsgStrs = []; + List srcMsgStrs = []; + + foreach (var actor in actors) + { + if (selectedArea.mLinkHolder.HasLinksWithDest(actor.mHash)) + { + var links = selectedArea.mLinkHolder.GetSrcHashesFromDest(actor.mHash); + + foreach (KeyValuePair> kvp in links) + { + var hashes = kvp.Value; + + foreach (var hash in hashes) + { + /* only delete actors that the hash exists for...this may be caused by a user already deleting the source actor */ + if (selectedArea.mActorHolder.TryGetActor(hash, out _)) + { + dstMsgStrs.Add($"{selectedArea.mActorHolder[hash].mPackName} [{selectedArea.mActorHolder[hash].mName}]\n"); + } + } + } + + var destHashes = selectedArea.mLinkHolder.GetDestHashesFromSrc(actor.mHash); + + foreach (KeyValuePair> kvp in destHashes) + { + var hashes = kvp.Value; + + foreach (var hash in hashes) + { + if (selectedArea.mActorHolder.TryGetActor(hash, out _)) + { + srcMsgStrs.Add($"{selectedArea.mActorHolder[hash].mPackName} [{selectedArea.mActorHolder[hash].mName}]\n"); + } + } + } + } + } + + bool noWarnings = (dstMsgStrs.Count == 0 && srcMsgStrs.Count == 0); + + if (!noWarnings) + { + var result = await OperationWarningDialog.ShowDialog(mPopupModalHost, + "Deletion warning", + "The object(s) you are about to delete " + + "are being used in other places", + ("As link source for", srcMsgStrs), + ("As link destination for", dstMsgStrs)); + + if (result == OperationWarningDialog.DialogResult.Cancel) + return; + } + + var batchAction = ctx.BeginBatchAction(); + + foreach (var actor in actors) + { + ctx.DeleteActor(actor); + } + + batchAction.Commit($"{IconUtil.ICON_TRASH} {actionName}"); + } + + private async Task AddActorsWithSelectActorAndLayerWindow() + { + var viewport = activeViewport; + var area = selectedArea; + var ctx = areaScenes[selectedArea].EditContext; + + if(mOpenToolWindows.Any(x=>x is SelectActorAndLayerWindow)) + return; + + var window = new SelectActorAndLayerWindow(mLayersVisibility); + mOpenToolWindows.Add(window); + + var result = await window.Result(); + if (!result.TryGetValue(out var resultVal)) + return; + + var (actorPack, layer) = resultVal; + + Vector3? pos; + KeyboardModifier modifier; + do + { + ImGui.SetWindowFocus(area.mAreaName); + (pos, modifier) = await viewport.PickPosition( + $"Placing actor {actorPack} -- Hold SHIFT to place multiple", layer); + if (!pos.TryGetValue(out var posVec)) + return; + + var actor = new CourseActor(actorPack, area.mRootHash, layer); + + posVec.X = MathF.Round(posVec.X * 2, MidpointRounding.AwayFromZero) / 2; + posVec.Y = MathF.Round(posVec.Y * 2, MidpointRounding.AwayFromZero) / 2; + posVec.Z = 0.0f; + actor.mTranslation = posVec; + + ctx.AddActor(actor); + + } while ((modifier & KeyboardModifier.Shift) > 0); + } + + + + interface IToolWindow + { + void Draw(ref bool windowOpen); + } + + class SelectActorAndLayerWindow(IReadOnlyDictionary mLayersVisibility) : IToolWindow + { + public void Draw(ref bool windowOpen) + { + bool status; + if (mSelectedActor == null) + { + status = ImGui.Begin("Add Actor###SelectActorLayer", ref windowOpen); + SelectActorToAdd(); + } + else if(mSelectedLayer == null) + { + status = ImGui.Begin("Select Layer###SelectActorLayer", ref windowOpen); + SelectActorToAddLayer(); + } + else + { + mPromise.TrySetResult((mSelectedActor, mSelectedLayer)); + windowOpen = false; + return; + } + + if (ImGui.IsKeyDown(ImGuiKey.Escape)) + { + windowOpen = false; + } + + if (!windowOpen) + { + mPromise.TrySetResult(null); + } + + if (status) + { + ImGui.End(); + } + + } + + private void SelectActorToAdd() + { + ImGui.InputText("Search", ref mAddActorSearchQuery, 256); + + var filteredActors = ParamDB.GetActors().ToImmutableList(); + + if (mAddActorSearchQuery != "") + { + filteredActors = FuzzySharp.Process.ExtractAll(mAddActorSearchQuery, ParamDB.GetActors(), cutoff: 65) + .OrderByDescending(result => result.Score) + .Select(result => result.Value) + .ToImmutableList(); + } + + if (ImGui.BeginListBox("Select the actor you want to add.", ImGui.GetContentRegionAvail())) + { + foreach (string actor in filteredActors) + { + ImGui.Selectable(actor); + + if (ImGui.IsItemHovered() && ImGui.IsMouseDoubleClicked(0)) + mSelectedActor = actor; + } + + ImGui.EndListBox(); + } + } + + private void SelectActorToAddLayer() + { + ImGui.InputText("Search", ref mAddLayerSearchQuery, 256); + + var fileteredLayers = mLayersVisibility.Keys.ToArray().ToImmutableList(); + + if (mAddLayerSearchQuery != "") + { + fileteredLayers = FuzzySharp.Process.ExtractAll(mAddLayerSearchQuery, mLayersVisibility.Keys.ToArray(), cutoff: 65) + .OrderByDescending(result => result.Score) + .Select(result => result.Value) + .ToImmutableList(); + } + + if (ImGui.BeginListBox("Select the layer you want to add the actor to.", ImGui.GetContentRegionAvail())) + { + foreach (string layer in fileteredLayers) + { + ImGui.Selectable(layer); + + if (ImGui.IsItemHovered() && ImGui.IsMouseDoubleClicked(0)) + mSelectedLayer = layer; + } + + ImGui.EndListBox(); + } + } + + public Task<(string actor, string layer)?> Result() => mPromise.Task; + + private string? mSelectedActor; + private string? mSelectedLayer; + private TaskCompletionSource<(string actor, string layer)?> mPromise = new(); + private string mAddActorSearchQuery = ""; + private string mAddLayerSearchQuery = ""; + } } } diff --git a/Fushigi/ui/widgets/LevelViewport.cs b/Fushigi/ui/widgets/LevelViewport.cs index 456deb5c..be9eaf34 100644 --- a/Fushigi/ui/widgets/LevelViewport.cs +++ b/Fushigi/ui/widgets/LevelViewport.cs @@ -48,8 +48,19 @@ interface ITransformableObject Transform Transform { get; } } + [Flags] + enum KeyboardModifier + { + None = 0, + Shift = 1, + CtrlCmd = 2, + Alt = 4 + } + internal class LevelViewport(CourseArea area, GL gl, CourseAreaScene areaScene) { + public event Action>? ObjectDeletionRequested; + readonly CourseArea mArea = area; //this is only so BgUnitRail works, TODO make private @@ -57,25 +68,16 @@ internal class LevelViewport(CourseArea area, GL gl, CourseAreaScene areaScene) ImDrawListPtr mDrawList; public EditorMode mEditorMode = EditorMode.Actors; - public EditorState mEditorState = EditorState.Selecting; public bool IsViewportHovered; public bool IsViewportActive; public bool IsWonderView; Vector2 mSize = Vector2.Zero; - + public ulong prevSelectVersion { get; private set; } = 0; - private Vector3? mSelectedPoint; - private int mWallIdx = -1; - private int mUnitIdx = -1; - private int mPointIdx = -1; private IDictionary? mLayersVisibility; Vector2 mTopLeft = Vector2.Zero; - public string mActorToAdd = ""; - public string mLayerToAdd = "PlayArea1"; - public bool mIsLinkNew = false; - public string mNewLinkType = ""; public Camera Camera = new Camera(); public GLFramebuffer Framebuffer; //Draws opengl data into the viewport @@ -83,22 +85,15 @@ internal class LevelViewport(CourseArea area, GL gl, CourseAreaScene areaScene) //TODO make this an ISceneObject? as soon as there's a SceneObj class for each course object private object? mHoveredObject; - public CourseLink? CurCourseLink = null; - public Vector3? HoveredPoint; - public uint GridColor = 0x77_FF_FF_FF; - public float GridLineThickness = 1.5f; + public static uint GridColor = 0x77_FF_FF_FF; + public static float GridLineThickness = 1.5f; - public enum EditorState - { - Selecting, - SelectingActorLayer, - AddingActor, - DeleteActorLinkCheck, - DeletingActor, - SelectingLinkSource, - SelectingLinkDest - } + private (string message, Predicate predicate, + TaskCompletionSource<(object? picked, KeyboardModifier modifiers)> promise)? + mObjectPickingRequest = null; + private (string message, string layer, TaskCompletionSource<(Vector3? picked, KeyboardModifier modifiers)> promise)? + mPositionPickingRequest = null; public enum EditorMode { @@ -106,6 +101,37 @@ public enum EditorMode Units } + public Task<(object? picked, KeyboardModifier modifiers)> PickObject(string tooltipMessage, + Predicate predicate) + { + CancelOngoingPickingRequests(); + var promise = new TaskCompletionSource<(object? picked, KeyboardModifier modifiers)>(); + mObjectPickingRequest = (tooltipMessage, predicate, promise); + return promise.Task; + } + + public Task<(Vector3? picked, KeyboardModifier modifiers)> PickPosition(string tooltipMessage, string layer) + { + CancelOngoingPickingRequests(); + var promise = new TaskCompletionSource<(Vector3? picked, KeyboardModifier modifiers)>(); + mPositionPickingRequest = (tooltipMessage, layer, promise); + return promise.Task; + } + + private void CancelOngoingPickingRequests() + { + if (mObjectPickingRequest.TryGetValue(out var objectPickingRequest)) + { + objectPickingRequest.promise.SetCanceled(); + mObjectPickingRequest = null; + } + if (mPositionPickingRequest.TryGetValue(out var positionPickingRequest)) + { + positionPickingRequest.promise.SetCanceled(); + mPositionPickingRequest = null; + } + } + public bool IsHovered(ISceneObject obj) => mHoveredObject == obj; public Matrix4x4 GetCameraMatrix() => Camera.ViewProjectionMatrix; @@ -314,6 +340,9 @@ private void RenderActor(CourseActor actor, ModelInfo modelInfo) public void Draw(Vector2 size, double deltaSeconds, IDictionary layersVisibility) { + if (size.X * size.Y == 0) + return; + mLayersVisibility = layersVisibility; mTopLeft = ImGui.GetCursorScreenPos(); @@ -323,8 +352,14 @@ public void Draw(Vector2 size, double deltaSeconds, IDictionary la IsViewportHovered = ImGui.IsItemHovered(); IsViewportActive = ImGui.IsItemActive(); - if (size.X * size.Y == 0) - return; + KeyboardModifier modifiers = KeyboardModifier.None; + + if(ImGui.GetIO().KeyShift) + modifiers |= KeyboardModifier.Shift; + if(ImGui.GetIO().KeyAlt) + modifiers |= KeyboardModifier.Alt; + if (OperatingSystem.IsMacOS() ? ImGui.GetIO().KeySuper : ImGui.GetIO().KeyCtrl) + modifiers |= KeyboardModifier.CtrlCmd; mSize = size; mDrawList = ImGui.GetWindowDrawList(); @@ -352,301 +387,175 @@ public void Draw(Vector2 size, double deltaSeconds, IDictionary la CourseActor? hoveredActor = mHoveredObject as CourseActor; - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + if (hoveredActor != null && + mObjectPickingRequest == null && mPositionPickingRequest == null) //prevents tooltip flickering + ImGui.SetTooltip($"{hoveredActor.mPackName}"); + + if (ImGui.IsKeyPressed(ImGuiKey.Z) && modifiers == KeyboardModifier.CtrlCmd) + { + mEditContext.Undo(); + } + if ((ImGui.IsKeyPressed(ImGuiKey.Y) && modifiers == KeyboardModifier.CtrlCmd) || + (ImGui.IsKeyPressed(ImGuiKey.Z) && modifiers == (KeyboardModifier.Shift | KeyboardModifier.CtrlCmd))) + { + mEditContext.Redo(); + } + + + + if (ImGui.IsWindowFocused()) + InteractionWithFocus(modifiers); + + ImGui.PopClipRect(); + } + + void InteractionWithFocus(KeyboardModifier modifiers) + { + if (IsViewportHovered && + mObjectPickingRequest.TryGetValue(out var objectPickingRequest)) { - if (hoveredActor != null) - ImGui.SetTooltip($"{hoveredActor.mPackName}"); + bool isValid = objectPickingRequest.predicate(mHoveredObject); - if (ImGui.IsKeyPressed(ImGuiKey.Z) && ImGui.GetIO().KeySuper) + string currentlyHoveredObjText = ""; + if (isValid && mHoveredObject is CourseActor hoveredActor) + currentlyHoveredObjText = $"\n\nCurrently Hovered: {hoveredActor.mPackName}"; + + ImGui.SetTooltip(objectPickingRequest.message + "\nPress Escape to cancel" + + currentlyHoveredObjText); + if (ImGui.IsKeyPressed(ImGuiKey.Escape)) { - mEditContext.Undo(); + mObjectPickingRequest = null; + objectPickingRequest.promise.SetResult((null, modifiers)); } - if (ImGui.IsKeyPressed(ImGuiKey.Y) && ImGui.GetIO().KeySuper || ImGui.IsKeyPressed(ImGuiKey.Z) && ImGui.GetIO().KeyShift && ImGui.GetIO().KeySuper) + else if (ImGui.IsMouseClicked(ImGuiMouseButton.Left) && + isValid) { - mEditContext.Redo(); + mObjectPickingRequest = null; + objectPickingRequest.promise.SetResult((mHoveredObject, modifiers)); } + + return; } - else + if (IsViewportHovered && + mPositionPickingRequest.TryGetValue(out var positionPickingRequest)) { - if (hoveredActor != null) - ImGui.SetTooltip($"{hoveredActor.mPackName}"); - - if (ImGui.IsKeyPressed(ImGuiKey.Z) && ImGui.GetIO().KeyCtrl) + ImGui.SetTooltip(positionPickingRequest.message + "\nPress Escape to cancel"); + if (ImGui.IsKeyPressed(ImGuiKey.Escape)) { - mEditContext.Undo(); + mPositionPickingRequest = null; + positionPickingRequest.promise.SetResult((null, modifiers)); } - if (ImGui.IsKeyPressed(ImGuiKey.Y) && ImGui.GetIO().KeyCtrl || ImGui.IsKeyPressed(ImGuiKey.Z) && ImGui.GetIO().KeyShift && ImGui.GetIO().KeyCtrl) + else if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { - mEditContext.Redo(); + //TODO use positionPickingRequest.layer + mPositionPickingRequest = null; + positionPickingRequest.promise.SetResult((ScreenToWorld(ImGui.GetMousePos()), modifiers)); } - }; - - - - bool isFocused = ImGui.IsWindowFocused(); - if (isFocused && mEditorState == EditorState.Selecting) + return; + } + + if (ImGui.IsMouseDragging(ImGuiMouseButton.Left)) { - if (ImGui.IsMouseDragging(ImGuiMouseButton.Left)) + if (mEditContext.IsAnySelected()) { - if (mEditContext.IsAnySelected()) - { - foreach(CourseActor actor in mEditContext.GetSelectedObjects()) - { - Vector3 posVec = ScreenToWorld(ImGui.GetMousePos()); - posVec -= ScreenToWorld(ImGui.GetIO().MouseClickedPos[0]) - actor.mStartingTrans; - - if (ImGui.GetIO().KeyShift) - { - actor.mTranslation = posVec; - } - else - { - posVec.X = MathF.Round(posVec.X * 2, MidpointRounding.AwayFromZero) / 2; - posVec.Y = MathF.Round(posVec.Y * 2, MidpointRounding.AwayFromZero) / 2; - posVec.Z = actor.mTranslation.Z; - actor.mTranslation = posVec; - } - } - } - if (mEditContext.IsSingleObjectSelected(out CourseRail.CourseRailPoint? rail)) + foreach(CourseActor actor in mEditContext.GetSelectedObjects()) { Vector3 posVec = ScreenToWorld(ImGui.GetMousePos()); + posVec -= ScreenToWorld(ImGui.GetIO().MouseClickedPos[0]) - actor.mStartingTrans; if (ImGui.GetIO().KeyShift) { - rail.mTranslate = posVec; + actor.mTranslation = posVec; } else { posVec.X = MathF.Round(posVec.X * 2, MidpointRounding.AwayFromZero) / 2; posVec.Y = MathF.Round(posVec.Y * 2, MidpointRounding.AwayFromZero) / 2; - posVec.Z = rail.mTranslate.Z; - rail.mTranslate = posVec; + posVec.Z = actor.mTranslation.Z; + actor.mTranslation = posVec; } } } - - - if (ImGui.IsItemClicked()) + if (mEditContext.IsSingleObjectSelected(out CourseRail.CourseRailPoint? rail)) { - bool isModeActor = mHoveredObject != null; - bool isModeUnit = HoveredPoint != null; - - if (isModeActor && !isModeUnit) - { - mEditorMode = EditorMode.Actors; - } - - if (isModeUnit && !isModeActor) - { - mEditorMode = EditorMode.Units; - } - - /* if the user clicked somewhere and it was not hovered over an element, - * we clear our selected actors array */ - if (mHoveredObject == null) - { - if(!ImGui.IsKeyDown(ImGuiKey.LeftShift)) - mEditContext.DeselectAll(); - } - else if (mHoveredObject is IViewportSelectable obj) - { - prevSelectVersion = mEditContext.SelectionVersion; - obj.OnSelect(mEditContext); - } - else - { - //TODO remove this once all course objects have IViewportSelectable SceneObjs - prevSelectVersion = mEditContext.SelectionVersion; - IViewportSelectable.DefaultSelect(mEditContext, mHoveredObject); - } + Vector3 posVec = ScreenToWorld(ImGui.GetMousePos()); - if (HoveredPoint == null) + if (ImGui.GetIO().KeyShift) { - mSelectedPoint = null; + rail.mTranslate = posVec; } else { - mSelectedPoint = HoveredPoint; - } - } - - if(mHoveredObject != null && mHoveredObject is CourseActor && - ImGui.IsMouseReleased(ImGuiMouseButton.Left)) - { - if (ImGui.GetIO().MouseDragMaxDistanceSqr[0] <= ImGui.GetIO().MouseDragThreshold) - { - if(ImGui.IsKeyDown(ImGuiKey.LeftShift) - && prevSelectVersion == mEditContext.SelectionVersion) - { - mEditContext.Deselect(mHoveredObject!); - } - else if(!ImGui.IsKeyDown(ImGuiKey.LeftShift)) - { - mEditContext.DeselectAll(); - IViewportSelectable.DefaultSelect(mEditContext, mHoveredObject); - } + posVec.X = MathF.Round(posVec.X * 2, MidpointRounding.AwayFromZero) / 2; + posVec.Y = MathF.Round(posVec.Y * 2, MidpointRounding.AwayFromZero) / 2; + posVec.Z = rail.mTranslate.Z; + rail.mTranslate = posVec; } } + } - if (ImGui.IsKeyPressed(ImGuiKey.Delete)) - { - mEditorState = EditorState.DeleteActorLinkCheck; - } - if (ImGui.IsKeyPressed(ImGuiKey.Escape)) - { - mEditContext.DeselectAll(); - mSelectedPoint = null; - } - } - else if (isFocused && mEditorState == EditorState.AddingActor) + if (ImGui.IsItemClicked()) { - ImGui.SetTooltip($"Placing actor {mActorToAdd} -- Hold SHIFT to place multiple, ESCAPE to cancel."); + bool isModeActor = mHoveredObject != null; + bool isModeUnit = HoveredPoint != null; - if (ImGui.IsKeyPressed(ImGuiKey.Escape)) + if (isModeActor && !isModeUnit) { - mEditorState = EditorState.Selecting; + mEditorMode = EditorMode.Actors; } - if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + if (isModeUnit && !isModeActor) { - CourseActor actor = new CourseActor(mActorToAdd, mArea.mRootHash, mLayerToAdd); - - Vector3 posVec = ScreenToWorld(ImGui.GetMousePos()); - posVec.X = MathF.Round(posVec.X * 2, MidpointRounding.AwayFromZero) / 2; - posVec.Y = MathF.Round(posVec.Y * 2, MidpointRounding.AwayFromZero) / 2; - posVec.Z = 0.0f; - actor.mTranslation = posVec; - - mEditContext.AddActor(actor); - - if (!ImGui.GetIO().KeyShift) - { - mActorToAdd = ""; - mEditorState = EditorState.Selecting; - } + mEditorMode = EditorMode.Units; } - } - else if (isFocused && mEditorState == EditorState.DeleteActorLinkCheck) - { - } - else if (isFocused && mEditorState == EditorState.DeletingActor) - { - if (!isFocused) + /* if the user clicked somewhere and it was not hovered over an element, + * we clear our selected actors array */ + if (mHoveredObject == null) { - ImGui.SetWindowFocus(); + if(!ImGui.IsKeyDown(ImGuiKey.LeftShift)) + mEditContext.DeselectAll(); } - - if (mEditContext.IsAnySelected()) + else if (mHoveredObject is IViewportSelectable obj) { - mEditContext.DeleteSelectedActors(); - mEditorState = EditorState.Selecting; + prevSelectVersion = mEditContext.SelectionVersion; + obj.OnSelect(mEditContext); } else { - if (hoveredActor != null) - ImGui.SetTooltip($""" - Click to delete {hoveredActor.mPackName}. - Hold SHIFT to delete multiple actors, ESCAPE to cancel. - """); - else - ImGui.SetTooltip(""" - Click on any actor to delete it. - Hold SHIFT to delete multiple actors, ESCAPE to cancel. - """); - - if (ImGui.IsKeyPressed(ImGuiKey.Escape)) - { - mEditorState = EditorState.Selecting; - } - - if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) - { - if (hoveredActor != null) - { - mEditContext.DeleteActor(hoveredActor); - - if (!ImGui.GetIO().KeyShift) - { - mEditorState = EditorState.Selecting; - } - } - } + //TODO remove this once all course objects have IViewportSelectable SceneObjs + prevSelectVersion = mEditContext.SelectionVersion; + IViewportSelectable.DefaultSelect(mEditContext, mHoveredObject); } } - else if (isFocused && (mEditorState == EditorState.SelectingLinkSource || mEditorState == EditorState.SelectingLinkDest)) - { - /* when we are begining to select a link, we will not always be immediately focused */ - if (!isFocused) - { - ImGui.SetWindowFocus(); - } - - if (ImGui.IsKeyPressed(ImGuiKey.Escape)) - { - mEditorState = EditorState.Selecting; - } - if (mEditorState == EditorState.SelectingLinkSource) - { - if (hoveredActor != null) - { - ImGui.SetTooltip($"Select the source actor you wish to link to. Press ESCAPE to cancel.\n Currently Hovered: {hoveredActor.mPackName}"); - } - else - { - ImGui.SetTooltip($"Select the source actor you wish to link to. Press ESCAPE to cancel."); - } - } - - if (mEditorState == EditorState.SelectingLinkDest) + if(mHoveredObject != null && mHoveredObject is CourseActor && + ImGui.IsMouseReleased(ImGuiMouseButton.Left)) + { + if (ImGui.GetIO().MouseDragMaxDistanceSqr[0] <= ImGui.GetIO().MouseDragThreshold) { - if (hoveredActor != null) + if(ImGui.IsKeyDown(ImGuiKey.LeftShift) + && prevSelectVersion == mEditContext.SelectionVersion) { - ImGui.SetTooltip($"Select the destination actor you wish to link to. Press ESCAPE to cancel.\n Currently Hovered: {hoveredActor.mPackName}"); + mEditContext.Deselect(mHoveredObject!); } - else + else if(!ImGui.IsKeyDown(ImGuiKey.LeftShift)) { - ImGui.SetTooltip($"Select the destination actor you wish to link to. Press ESCAPE to cancel."); + mEditContext.DeselectAll(); + IViewportSelectable.DefaultSelect(mEditContext, mHoveredObject); } } + } - /* if our link is new, it means that we don't have to check for hovered actors for the source designation */ - if (mIsLinkNew) - { - CurCourseLink = new(mNewLinkType); - CourseActor selActor = mEditContext.GetSelectedObjects().ElementAt(0); - CurCourseLink.mSource = selActor.mHash; - mIsLinkNew = false; - } - - if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) - { - if (mEditorState == EditorState.SelectingLinkDest) - { - if (hoveredActor != null) - { - /* new links have a destination of 0 because there is no hash associated with a null actor */ - bool isNewLink = CurCourseLink.mDest == 0; - ulong hash = hoveredActor.mHash; - CurCourseLink.mDest = hash; - - if (isNewLink) - { - mEditContext.AddLink(CurCourseLink); - } - - mEditorState = EditorState.Selecting; - } - } - - } + if (ImGui.IsKeyPressed(ImGuiKey.Delete)) + { + mEditorState = EditorState.DeleteActorLinkCheck; } - ImGui.PopClipRect(); + if (ImGui.IsKeyPressed(ImGuiKey.Escape)) + mEditContext.DeselectAll(); } void DrawGrid() diff --git a/Fushigi/ui/widgets/OperationWarningDialog.cs b/Fushigi/ui/widgets/OperationWarningDialog.cs new file mode 100644 index 00000000..f52820ef --- /dev/null +++ b/Fushigi/ui/widgets/OperationWarningDialog.cs @@ -0,0 +1,113 @@ +using Fushigi.ui.modal; +using Fushigi.util; +using ImGuiNET; +using System.Numerics; + +namespace Fushigi.ui.widgets +{ + class OperationWarningDialog : IPopupModal + { + public enum DialogResult + { + OK, + Cancel + } + + public static async Task ShowDialog(IPopupModalHost modalHost, + string title, string warning, + params (string category, IReadOnlyList warnings)[] categorizedWarnings) + { + var result = await modalHost.ShowPopUp( + new OperationWarningDialog(warning, categorizedWarnings), + title, ImGuiWindowFlags.AlwaysAutoResize); + + if (result.wasClosed) + return DialogResult.Cancel; + + return result.result; + } + + public OperationWarningDialog(string warning, + (string category, IReadOnlyList warnings)[] categorizedWarnings) + { + mWarning = warning; + mCategorizedWarnings = categorizedWarnings; + } + + public void DrawModalContent(Promise promise) + { + ImGui.Text(mWarning); + ImGui.Separator(); + + #region scrollarea with sticky headers + float width = ImGui.GetContentRegionAvail().X; + ImGui.SetNextWindowSizeConstraints(new Vector2(width, 0), + new Vector2(width, ImGui.GetWindowViewport().WorkSize.Y / 3f)); + ImGui.SetNextWindowPos(ImGui.GetCursorScreenPos()); + ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 0); + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(0)); + var cursorPos = ImGui.GetCursorPos(); + if (ImGui.Begin("CategorizedWarnings", + ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoNavFocus)) + { + var headerStickPosY = 0f; + foreach (var (category, warnings) in mCategorizedWarnings) + { + if (warnings.Count == 0) + continue; + float offset = 0; + if(ImGui.GetCursorPosY() - ImGui.GetScrollY() < headerStickPosY) + { + var newCursorY = ImGui.GetScrollY() + headerStickPosY; + offset = newCursorY - ImGui.GetCursorPosY(); + ImGui.SetCursorPosY(newCursorY); + } + + bool expanded = ImGui.CollapsingHeader($"{category} ({warnings.Count})", + ImGuiTreeNodeFlags.DefaultOpen); + + if (ImGui.IsItemClicked()) + ImGui.SetScrollY(ImGui.GetScrollY() - offset); + + if(!expanded) + continue; + + ImGui.PushClipRect(ImGui.GetCursorScreenPos(), + new Vector2(float.PositiveInfinity), true); + + headerStickPosY = ImGui.GetCursorPosY() - ImGui.GetScrollY(); + + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - offset); + + ImGui.Indent(); + foreach (string w in warnings) + { + ImGui.Text(w); + ImGui.Separator(); + } + ImGui.Unindent(); + ImGui.PopClipRect(); + ImGui.SetWindowSize(new Vector2(100, 100)); + } + cursorPos += ImGui.GetWindowSize() with { X = 0 }; + ImGui.End(); + } + ImGui.PopStyleVar(2); + ImGui.SetCursorPos(cursorPos); + #endregion + + + ImGui.Spacing(); + + if (ImGui.Button("Continue anyway")) + promise.SetResult(DialogResult.OK); + + ImGui.SameLine(); + + if (ImGui.Button("Cancel")) + promise.SetResult(DialogResult.Cancel); + } + private readonly string mWarning; + private readonly (string category, IReadOnlyList warnings)[] mCategorizedWarnings; + } +}