C# Windows Forms tab navigation broken when form registered with NPPM_MODELESSDIALOG
-
Since the changes in Npp 8.6.1 that changed Ctrl+X and Ctrl+C into menu commands, I’ve been trying to figure out a decent solution that makes Ctrl+C and Ctrl+X work again in my C# plugins.
The “correct” solution to this problem is to register and unregister each of my forms with
NPPM_MODELESSDIALOG
, and I understand how to do this. Doing so does indeed restore the functionality of Ctrl+C and Ctrl+X in Npp 8.6.1 and 8.6.2, but it also causes tab navigation to break, which is …unexpected.To be brief, in C# Windows Forms, controls have a
TabIndex
property that controls their position in the tab order. It is my understanding that things work differently in C++ (like I think you can choose either a top-to-bottom-then-left-to-right or left-to-right-then-top-to-bottom algorithm?), and I would be willing to use NPPM_MODELESSDIALOG if the new order of tab navigation made any sense at all, but it really doesn’t. I will show a diagram of the before and after for one of my forms (it’s a representative example; all my forms get messed up like this).This is the order of the controls by tab index (I chose a left-to-right-then-top-to-bottom order, more or less)
And this is the order that I get when I register my form with
NPPM_MODELESSDIALOG
:
Do you see a pattern in the second one? Because I sure don’t. And before you ask, the weird order you see there is not the order in which the controls are declared or initialized, as far as I can tell.
My current thinking (very vague and speculative and most likely wrong somehow, because I’m not really experienced with C++) is that in winmain.cpp in the message loop there’s a call to
TranslateMessage
andDispatchMessageW
after an earlier call (inNotepad_plus_Window::isDlgsMsg
) toIsDialogMessageW
, but the standard documentation forIsDialogMessageW
specifically says that:Because the IsDialogMessage function performs all necessary translating and dispatching of messages, a message processed by IsDialogMessage must not be passed to the TranslateMessage or DispatchMessage function.
-
@Mark-Olson said in C# Windows Forms tab navigation broken when form registered with NPPM_MODELESSDIALOG:
To be brief, in C# Windows Forms, controls have a
TabIndex
property that controls their position in the tab order. It is my understanding that things work differently in C++Not really. There is a tab order; when building a dialog using the resource editor in Visual Studio, you can set the order to be any sequence you like. What that does is control the order in which the controls are defined in the resource file (which is compiled to produce, among other things, dialog templates), which determines the tab order.
My current thinking (very vague and speculative and most likely wrong somehow, because I’m not really experienced with C++) is that in winmain.cpp in the message loop there’s a call to
TranslateMessage
andDispatchMessageW
after an earlier call (inNotepad_plus_Window::isDlgsMsg
) toIsDialogMessageW
, but the standard documentation forIsDialogMessageW
specifically says that:Because the IsDialogMessage function performs all necessary translating and dispatching of messages, a message processed by IsDialogMessage must not be passed to the TranslateMessage or DispatchMessage function.
The further processing only happens when IsDlgsMsg returns false; when IsDialogMessageW returns true, so does IsDlgsMsg.
It’s not clear to me what could cause this sort of behavior, but I don’t know C# and don’t how it integrates with “plain old Windows.” I noticed that your modal dialog “JSON to CSV” has some odd tabbing behavior — it appears to tab once on key down or repeat and again on key up — and as a modal dialog, it is unaffected by the Notepad++ message loop. Just a long shot: perhaps whatever is causing that odd behavior is somehow related to the unexpected behavior in non-modal dialogs.
-
@Mark-Olson said in C# Windows Forms tab navigation broken when form registered with NPPM_MODELESSDIALOG:
Do you see a pattern in the second one?
Looks like the order here:
https://github.com/molsonkiko/JsonToolsNppPlugin/blob/929188b0b9cfd9231ea6a2323921ba026a371786/JsonToolsNppPlugin/Forms/FindReplaceForm.Designer.cs#L284
and here:
https://github.com/molsonkiko/JsonToolsNppPlugin/blob/929188b0b9cfd9231ea6a2323921ba026a371786/JsonToolsNppPlugin/Forms/FindReplaceForm.Designer.cs#L219considering that you start with focus on FindTextBox, loop, and “call into” the second set of adds when you hit AdvancedGroupBox.
-
Had a quick look and the issue seems to be the
KeyUp
handler, specifically theGenericTabNavigationHandler
method which it calls in response to aTAB
.When you register the form by sending
NPPM_MODELESSDIALOG
, the return value ofform.GetNextControl()
is a reference to the form itself, and the “next” control to get focus is the “Replace all” button.When it’s not registered,
form.GetNextControl()
returns a reference to the form’s next child component, currently the “Swap” button, and so on, as illustrated in the first screen capture above.You could maybe try iterating the form’s
Controls
collection instead of depending on tab order, as suggested by the docs.As for why
NPPM_MODELESSDIALOG
appears to change the component hierarchy, it could be another case of theWS_EX_CONTROLPARENT
flag needing to be set on the parent form’s window attributes to make the OS aware that it is, in fact, a parent control. -
After a closer look there seems to be more going on here.
Without NPPM_MODELESSDIALOG, every control on the form receives its own key events. When a
TextBox
has focus, the sender object passed toGenericTabNavigationHandler
is thatTextBox
, and the next tab stop is relative to thatTextBox
, and so on for every focusable control.The normal sequence of a key event should be WM_KEYDOWN (handled by .NET’s
KeyDown
) -> WM_CHAR (i.e.KeyPress
) -> WM_KEYUP (i.e.KeyUP
), but that assumes the receiver is some kind of edit control. An instance ofForm
doesn’t know about a key event until a child component broadcasts its own WM_KEYUP message to its parent; I don’t know how or if a parent can stop a child from processing it.After sending NPPM_MODELESSDIALOG, only the main form instance receives messages, and it’s only receptive to WM_KEYUP, so while the .NET
KeyUp
handler still works, all the others are dormant. Since the only events are coming from the main form, the next tab stop is always relative to it. There’s a loop inGenericTabNavigationHandler
that will find a valid component in any case, but what it lands on will have nothing in common with the design-time tab order. You can even block theKeyUp
handler’s code path, and focus will still jump around, suggesting some controls are silently processing tab keys.To see for yourself, first use the
System.Diagnostics
namespace and set up debug logging inside a static constructor, e.g.,static FindReplaceForm() { Debug.Listeners.Add(new TextWriterTraceListener(System.Console.Out)); Debug.AutoFlush = true; }
Then override
ProcessKeyPreview
, like this:protected override bool ProcessKeyPreview(ref Message msg) { var keyCode = (Keys)(byte)msg.WParam; switch (msg.Msg) { case /*WM_KEYDOWN*/ 0x100: Debug.WriteLine("WM_KEYDOWN"); break; case /*WM_KEYUP*/ 0x101: Debug.WriteLine("WM_KEYUP"); if (keyCode == Keys.Tab) { Debug.WriteLine("Handling TAB . . ."); // 1. send NPPM_MODELESSDIALOG, and: // a. WM_KEYUP is the only message ever received // b. TAB still moves focus around the form despite returning `true` here // 2. don't send NPPM_MODELESSDIALOG, and: // a. we receive all of WM_KEYDOWN, WM_CHAR (handled by KeyPress), and WM_KEYUP, in that order // b. all key processing ends here if we return `true` return true; } break; case /*WM_CHAR*/ 0x102: Debug.WriteLine("WM_CHAR"); break; case /*WM_SYSKEYDOWN*/ 0x104: Debug.WriteLine("WM_SYSKEYDOWN"); break; case /*WM_SYSKEYUP*/ 0x105: Debug.WriteLine("WM_SYSKEYUP"); break; default: break; } return base.ProcessKeyPreview(ref msg); }
-
@rdipardo said in C# Windows Forms tab navigation broken when form registered with NPPM_MODELESSDIALOG:
As for why NPPM_MODELESSDIALOG appears to change the component hierarchy, it could be another case of the WS_EX_CONTROLPARENT flag needing to be set on the parent form’s window attributes to make the OS aware that it is, in fact, a parent control.
Based on @Mark-Olson’s second image, nothing is being changed; it’s exactly the order in which components are added here and here.
I don’t even know how to set up a C# project, but by analogy to what I would say for a C++ project: has anyone tried making the order in which controls are added in FindReplaceForm.Designer.cs reflect the intended tab order, then letting the default dialog procedure (or whatever is the C# equivalent of that) handle navigation and not interfering by using keyboard event handlers? (Perhaps the keyboard event handlers were added in the first place because navigation in a non-modal dialog doesn’t work normally unless the message loop calls IsDialogMessage, which for a Notepad++ plugin requires using NPPM_MODELESSDIALOG.)
-
Thank you both for your suggestions! I meant to respond sooner, but there was a power outage.
In any case, Coises’ original suggestion worked! Thank you so much! I already knew that I had to disable
GenericTabNavigationHandler
, so all I did was go into theDesigner.cs
files for all of my forms and rearrange the order in which they were added to their parent and it just worked.Hand-editing
Designer.cs
files is kind of a no-no, because they’re automatically generated by Visual Studio when you edit a form in the designer, but I’ll happily take this solution over whatever lousy alternative I was going to use.