Skip to content

Commit

Permalink
IDE: Use proper owner on message boxes.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jordanrussell authored Dec 8, 2024
1 parent c3563a2 commit 72dd250
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 5 deletions.
25 changes: 25 additions & 0 deletions Projects/Src/IDE.MainForm.pas
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 }
Expand Down
37 changes: 32 additions & 5 deletions Projects/Src/Shared.CommonFunc.Vcl.pas
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 72dd250

Please sign in to comment.