From 72dd250bb1ca19bd1e6d3a48998722a4df059944 Mon Sep 17 00:00:00 2001 From: Jordan Russell Date: Sun, 8 Dec 2024 04:50:44 -0600 Subject: [PATCH] IDE: Use proper owner on message boxes. AppMessageBox wasn't compatible with MainFormOnTaskBar=True; it displayed every message box with no owner. Also found that the VCL doesn't set the owner correctly on dialogs displayed when the app isn't in the foreground. Workaround implemented. Left the IsIconic call (from #164) in for now, though I don't think it's needed anymore in the IDE as long as message boxes use the main form as their owner. It may still be needed by Setup; haven't yet checked. --- Projects/Src/IDE.MainForm.pas | 25 +++++++++++++++++ Projects/Src/Shared.CommonFunc.Vcl.pas | 37 ++++++++++++++++++++++---- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/Projects/Src/IDE.MainForm.pas b/Projects/Src/IDE.MainForm.pas index 2dcbdb41f..be5e627a1 100644 --- a/Projects/Src/IDE.MainForm.pas +++ b/Projects/Src/IDE.MainForm.pas @@ -504,6 +504,7 @@ TMainForm = class(TUIStateForm) function AnyMemoHasBreakPoint: Boolean; class procedure AppOnException(Sender: TObject; E: Exception); procedure AppOnActivate(Sender: TObject); + class procedure AppOnGetActiveFormHandle(var AHandle: HWND); procedure AppOnIdle(Sender: TObject; var Done: Boolean); function AskToDetachDebugger: Boolean; procedure BringToForeground; @@ -1184,6 +1185,29 @@ class procedure TMainForm.AppOnException(Sender: TObject; E: Exception); MB_OK or MB_ICONSTOP); end; +class procedure TMainForm.AppOnGetActiveFormHandle(var AHandle: HWND); +begin + { As of Delphi 11.3, the default code in TApplication.GetActiveFormHandle + (which runs after this handler) calls GetActiveWindow, and if that returns + 0, it calls GetLastActivePopup(Application.Handle). + The problem is that when the application isn't in the foreground, + GetActiveWindow returns 0, and when MainFormOnTaskBar=True, the + GetLastActivePopup call normally just returns Application.Handle (since + there are no popups owned by the application window). + So if the application calls Application.MessageBox while it isn't in the + foreground, that message box will be owned by Application.Handle, not by + the last-active form as it should be. That can lead to the message box + falling behind the form in z-order. + To rectify that, we return Screen.ActiveForm.Handle if possible, which is + valid whether or not the application is in the foreground. This code is + from TCustomTaskDialog.Execute. (HandleAllocated call added to be safe) } + + if Assigned(Screen.ActiveForm) and + (Screen.ActiveForm.FormStyle <> fsMDIChild) and + Screen.ActiveForm.HandleAllocated then + AHandle := Screen.ActiveForm.Handle; +end; + procedure TMainForm.FormAfterMonitorDpiChanged(Sender: TObject; OldDPI, NewDPI: Integer); begin @@ -7852,6 +7876,7 @@ function TMainForm.FromCurrentPPI(const XY: Integer): Integer; end; initialization + Application.OnGetActiveFormHandle := TMainForm.AppOnGetActiveFormHandle; InitThemeLibrary; InitHtmlHelpLibrary; { For ClearType support, try to make the default font Microsoft Sans Serif } diff --git a/Projects/Src/Shared.CommonFunc.Vcl.pas b/Projects/Src/Shared.CommonFunc.Vcl.pas index 3d2ac2e08..da1dcd0b5 100644 --- a/Projects/Src/Shared.CommonFunc.Vcl.pas +++ b/Projects/Src/Shared.CommonFunc.Vcl.pas @@ -212,16 +212,43 @@ function AppMessageBox(const Text, Caption: PChar; Flags: Longint): Integer; ActiveWindow: HWND; WindowList: Pointer; begin + { Always try to bring the message box to the foreground. Task dialogs appear + to do that by default. + Without this, if the main form is minimized and then closed via the + taskbar 'X', a message box shown in its OnCloseQuery handler gets + displayed behind the foreground app's window, with no indication that a + message box is waiting. With the flag set, the message box is still shown + behind the foreground app's window, but the taskbar button begins blinking + and the main form is restored automatically. (These tests were done with + MainFormOnTaskBar=True and the message box window properly owned by the + main form. Don't run under the debugger when testing because that changes + the foreground stealing rules.) } + Flags := Flags or MB_SETFOREGROUND; if MessageBoxRightToLeft then Flags := Flags or (MB_RTLREADING or MB_RIGHT); TriggerMessageBoxCallbackFunc(Flags, False); try - { If the application window isn't currently visible, show the message box - with no owner window so it'll get a taskbar button } - if IsIconic(Application.Handle) or - (GetWindowLong(Application.Handle, GWL_STYLE) and WS_VISIBLE = 0) or - (GetWindowLong(Application.Handle, GWL_EXSTYLE) and WS_EX_TOOLWINDOW <> 0) then begin + { Application.MessageBox uses Application.ActiveFormHandle for the message + box's owner window. If that window is Application.Handle AND it isn't + currently shown on the taskbar [1], the result will be a message box + with no taskbar button -- which can easily get lost behind other + windows. Avoid that by calling MessageBox directly with no owner window. + [1] That is the case when we're called while no forms are visible. + But it can also be the case when Application.MainFormOnTaskBar=True + and we're called while the application isn't in the foreground + (i.e., GetActiveWindow=0). That seems like erroneous behavior on the + VCL's part (it should return the same handle as when the app is in + the foreground), and it causes modal TForms to get the 'wrong' owner + as well. However, it can be worked around using a custom + Application.OnGetActiveFormHandle handler. } + var ActWnd := Application.ActiveFormHandle; + if ActWnd = 0 then { shouldn't be possible, but they have this check } + ActWnd := Application.Handle; + if (ActWnd = Application.Handle) and + (IsIconic(Application.Handle) or + (GetWindowLong(Application.Handle, GWL_STYLE) and WS_VISIBLE = 0) or + (GetWindowLong(Application.Handle, GWL_EXSTYLE) and WS_EX_TOOLWINDOW <> 0)) then begin ActiveWindow := GetActiveWindow; WindowList := DisableTaskWindows(0); try