From 273bfed6ee3cfe6a49007a024b52bb4d3f560e51 Mon Sep 17 00:00:00 2001 From: Zachary Burnham Date: Wed, 5 Aug 2026 10:10:12 -0400 Subject: [PATCH] LT-22578: Dialog window Z order fixes The dialogs for creating a new inflectional affix could appear behind their parent window. This change fixes this and similar dialog situations for LinkEntryOrSenseDlg and EntryGoDlg. https://jira.sil.org/browse/LT-22578 --- .../Controls/DetailControls/ChooserCommand.cs | 9 ++++ ...erCommandKeepOwnerActiveWhenHidingTests.cs | 44 +++++++++++++++++++ .../Controls/XMLViews/ChooserCommandBase.cs | 14 ++++++ .../XMLViews/ReallySimpleListChooser.cs | 25 ++++++++++- .../Lexicon/EntrySequenceReferenceLauncher.cs | 22 +++++++++- ...erCommandKeepOwnerActiveWhenHidingTests.cs | 43 ++++++++++++++++++ .../Morphology/InflAffixTemplateControl.cs | 3 +- 7 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 Src/Common/Controls/DetailControls/DetailControlsTests/ChooserCommandKeepOwnerActiveWhenHidingTests.cs create mode 100644 Src/LexText/Lexicon/LexEdDllTests/EntrySequenceChooserCommandKeepOwnerActiveWhenHidingTests.cs diff --git a/Src/Common/Controls/DetailControls/ChooserCommand.cs b/Src/Common/Controls/DetailControls/ChooserCommand.cs index 16f0a3bf57..c58625d5e3 100644 --- a/Src/Common/Controls/DetailControls/ChooserCommand.cs +++ b/Src/Common/Controls/DetailControls/ChooserCommand.cs @@ -33,6 +33,15 @@ public MakeInflAffixEntryChooserCommand(LcmCache cache, bool fCloseBeforeExecuti m_slot = slot; } + /// + /// This command opens the modal New Entry dialog, so have the chooser keep the main window + /// active as it hides, to avoid another application flashing in front first. + /// + public override bool KeepOwnerActiveWhenHiding + { + get { return true; } + } + //methods public override ObjectLabel Execute() diff --git a/Src/Common/Controls/DetailControls/DetailControlsTests/ChooserCommandKeepOwnerActiveWhenHidingTests.cs b/Src/Common/Controls/DetailControls/DetailControlsTests/ChooserCommandKeepOwnerActiveWhenHidingTests.cs new file mode 100644 index 0000000000..e50328ea69 --- /dev/null +++ b/Src/Common/Controls/DetailControls/DetailControlsTests/ChooserCommandKeepOwnerActiveWhenHidingTests.cs @@ -0,0 +1,44 @@ +// Copyright (c) 2026 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using NUnit.Framework; + +namespace SIL.FieldWorks.Common.Framework.DetailControls +{ + /// + /// Guards the KeepOwnerActiveWhenHiding opt-in contract for the affix chooser commands + /// (LT-22578). A chooser command whose Execute() opens another modal dialog must return true + /// so ReallySimpleListChooser.HideForCommand re-activates the FLEx main window as the chooser + /// hides; without it an unrelated application can flash in front while the new dialog loads. A + /// command that opens no dialog must stay false (the base default). The flag is the only part + /// of the fix that is deterministic and UI-free; the actual owner activation and window + /// Z-order require a live message pump and are verified manually. + /// + [TestFixture] + public class ChooserCommandKeepOwnerActiveWhenHidingTests + { + /// + /// Opens the modal New Entry dialog (InsertEntryDlg), so it must opt in. + /// + [Test] + public void MakeInflAffixEntryChooserCommand_OptsIn() + { + var command = new MakeInflAffixEntryChooserCommand( + null, true, "label", true, null, null, null); + Assert.That(command.KeepOwnerActiveWhenHiding, Is.True); + } + + /// + /// Creates a slot without opening a dialog, so it keeps the base default of false. This + /// also exercises the base ChooserCommand default (this command does not override it). + /// + [Test] + public void MakeInflAffixSlotChooserCommand_DoesNotOptIn() + { + var command = new MakeInflAffixSlotChooserCommand( + null, true, "label", 0, false, null, null); + Assert.That(command.KeepOwnerActiveWhenHiding, Is.False); + } + } +} diff --git a/Src/Common/Controls/XMLViews/ChooserCommandBase.cs b/Src/Common/Controls/XMLViews/ChooserCommandBase.cs index 49d7273f19..90cb7d7894 100644 --- a/Src/Common/Controls/XMLViews/ChooserCommandBase.cs +++ b/Src/Common/Controls/XMLViews/ChooserCommandBase.cs @@ -97,6 +97,20 @@ public bool ShouldCloseBeforeExecuting m_fShouldCloseBeforeExecuting = value; } } + + /// + /// When true, the chooser re-enables its owner window just before hiding itself to run this + /// command. Execute() for such a command opens another modal dialog, which re-disables the + /// owner; re-enabling it first makes the OS return activation to the FLEx main window when + /// the chooser hides, instead of momentarily revealing an unrelated application's window. + /// Defaults to false so behavior is unchanged for every command that does not opt in. See + /// the "Create new inflectional affix" flow (LT-22578). + /// + public virtual bool KeepOwnerActiveWhenHiding + { + get { return false; } + } + /// /// The entire text of the label that will appear in the chooser /// diff --git a/Src/Common/Controls/XMLViews/ReallySimpleListChooser.cs b/Src/Common/Controls/XMLViews/ReallySimpleListChooser.cs index 797acc7745..0e00b335da 100644 --- a/Src/Common/Controls/XMLViews/ReallySimpleListChooser.cs +++ b/Src/Common/Controls/XMLViews/ReallySimpleListChooser.cs @@ -2281,6 +2281,27 @@ private void InitializeComponent() } #endregion + /// + /// Hide the chooser before running . When the command opts in via + /// KeepOwnerActiveWhenHiding (its Execute() opens another modal dialog), re-enable and then + /// re-activate our owner around the hide. While the chooser is modal the OS has disabled the + /// owner, so hiding alone would hand the foreground to another application (the next window + /// by z-order) instead of back to the FLEx main window, causing a brief flash. Re-enabling is + /// not enough on its own; the Activate() is what pulls FLEx forward, and it is not blocked by + /// the foreground lock because we are still the foreground process here (LT-22578). + /// + private void HideForCommand(ChooserCommand cmd) + { + if (cmd != null && cmd.KeepOwnerActiveWhenHiding && Owner != null) + { + Owner.Enabled = true; + Visible = false; + Owner.Activate(); + return; + } + Visible = false; + } + private void HandleCommmandChoice(ChooserCommandNode node) { if (node != null) @@ -2289,7 +2310,7 @@ private void HandleCommmandChoice(ChooserCommandNode node) if (cmd != null) { if (cmd.ShouldCloseBeforeExecuting) - Visible = false; + HideForCommand(cmd); m_chosenLabel = cmd.Execute(); } } @@ -2300,7 +2321,7 @@ private void OnOKClick(object sender, EventArgs e) Persist(); if (m_linkCmd != null) { - Visible = false; + HideForCommand(m_linkCmd); m_chosenLabel = m_linkCmd.Execute(); m_fLinkExecuted = true; } diff --git a/Src/LexText/Lexicon/EntrySequenceReferenceLauncher.cs b/Src/LexText/Lexicon/EntrySequenceReferenceLauncher.cs index 4ac6a6f80c..763c77d072 100644 --- a/Src/LexText/Lexicon/EntrySequenceReferenceLauncher.cs +++ b/Src/LexText/Lexicon/EntrySequenceReferenceLauncher.cs @@ -126,7 +126,7 @@ protected override void HandleChooser() chooser.InitializeExtras(null, Mediator, m_propertyTable); chooser.AddLink(LexEdStrings.ksAddAComponent, ReallySimpleListChooser.LinkType.kDialogLink, new AddPrimaryLexemeChooserCommand(m_cache, false, null, m_mediator, m_propertyTable, m_obj, FindForm())); - DialogResult res = chooser.ShowDialog(); + DialogResult res = chooser.ShowDialog(FindForm()); if (DialogResult.Cancel == res) return; if (chooser.ChosenObjects != null) @@ -204,7 +204,7 @@ private void HandleChooserForBackRefs(string fieldName, bool fPropContainsEntryR // Step 3 of LT-11155: chooser.AddLink(LexEdStrings.ksAddAComplexForm, ReallySimpleListChooser.LinkType.kDialogLink, new AddComplexFormChooserCommand(m_cache, false, null, m_mediator, m_propertyTable, m_obj, FindForm())); - DialogResult res = chooser.ShowDialog(); + DialogResult res = chooser.ShowDialog(FindForm()); if (DialogResult.Cancel == res) return; var chosenObjects = chooser.ChosenObjects; @@ -353,6 +353,15 @@ public AddPrimaryLexemeChooserCommand(LcmCache cache, bool fCloseBeforeExecuting m_parentWindow = parentWindow; } + /// + /// This command opens the modal LinkEntryOrSenseDlg, so have the chooser keep the main + /// window active as it hides, to avoid another application flashing in front first. + /// + public override bool KeepOwnerActiveWhenHiding + { + get { return true; } + } + public override ObjectLabel Execute() { ObjectLabel result = null; @@ -426,6 +435,15 @@ public AddComplexFormChooserCommand(LcmCache cache, bool fCloseBeforeExecuting, m_parentWindow = parentWindow; } + /// + /// This command opens the modal EntryGoDlg, so have the chooser keep the main window + /// active as it hides, to avoid another application flashing in front first. + /// + public override bool KeepOwnerActiveWhenHiding + { + get { return true; } + } + public override ObjectLabel Execute() { ObjectLabel result = null; diff --git a/Src/LexText/Lexicon/LexEdDllTests/EntrySequenceChooserCommandKeepOwnerActiveWhenHidingTests.cs b/Src/LexText/Lexicon/LexEdDllTests/EntrySequenceChooserCommandKeepOwnerActiveWhenHidingTests.cs new file mode 100644 index 0000000000..8435dc19c7 --- /dev/null +++ b/Src/LexText/Lexicon/LexEdDllTests/EntrySequenceChooserCommandKeepOwnerActiveWhenHidingTests.cs @@ -0,0 +1,43 @@ +// Copyright (c) 2026 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using NUnit.Framework; +using SIL.FieldWorks.XWorks.LexEd; + +namespace LexEdDllTests +{ + /// + /// Guards the KeepOwnerActiveWhenHiding opt-in contract for the Lexicon "Add a Component" and + /// "Add a Complex Form" chooser commands (LT-22578). Both are launched from a chooser that + /// hides itself and then open a modal dialog (LinkEntryOrSenseDlg / EntryGoDlg), so both must + /// opt in to keep an unrelated application from flashing in front while the dialog loads. See + /// ChooserCommandKeepOwnerActiveWhenHidingTests (DetailControls) for the affix equivalents and + /// the rationale for why only the opt-in flag is unit-tested. + /// + [TestFixture] + public class EntrySequenceChooserCommandKeepOwnerActiveWhenHidingTests + { + /// + /// "Add a Component" opens the modal LinkEntryOrSenseDlg, so it must opt in. + /// + [Test] + public void AddPrimaryLexemeChooserCommand_OptsIn() + { + var command = new AddPrimaryLexemeChooserCommand( + null, false, "label", null, null, null, null); + Assert.That(command.KeepOwnerActiveWhenHiding, Is.True); + } + + /// + /// "Add a Complex Form" opens the modal EntryGoDlg, so it must opt in. + /// + [Test] + public void AddComplexFormChooserCommand_OptsIn() + { + var command = new AddComplexFormChooserCommand( + null, false, "label", null, null, null, null); + Assert.That(command.KeepOwnerActiveWhenHiding, Is.True); + } + } +} diff --git a/Src/LexText/Morphology/InflAffixTemplateControl.cs b/Src/LexText/Morphology/InflAffixTemplateControl.cs index d6bdce81a8..41d1da865e 100644 --- a/Src/LexText/Morphology/InflAffixTemplateControl.cs +++ b/Src/LexText/Morphology/InflAffixTemplateControl.cs @@ -412,7 +412,8 @@ public bool OnInflTemplateAddInflAffixMsa(object cmd) using (var chooser = MakeChooserWithExtantMsas(m_slot, cmd as XCore.Command)) { - chooser.ShowDialog(this); + // Own the chooser to the top-level main window, not `this` child control. + chooser.ShowDialog(m_propertyTable.GetValue
("window")); if (chooser.DialogResult == DialogResult.OK) { if (chooser.ChosenObjects != null && chooser.ChosenObjects.Count() > 0)