Bug 439137 - GTK4 child windows/dialogs positioned incorrectly on KWin X11
Summary: GTK4 child windows/dialogs positioned incorrectly on KWin X11
Status: RESOLVED UPSTREAM
Alias: None
Product: kwin
Classification: Plasma
Component: general (show other bugs)
Version: 5.22.2
Platform: unspecified Linux
: NOR normal
Target Milestone: ---
Assignee: KWin default assignee
URL: https://gitlab.gnome.org/GNOME/gtk/-/...
Keywords:
Depends on:
Blocks:
 
Reported: 2021-06-25 03:31 UTC by nyanpasu64
Modified: 2021-07-30 17:04 UTC (History)
3 users (show)

See Also:
Latest Commit:
Version Fixed In:


Attachments
There are chances that the dialog window locates in the center of the parent window. (146.82 KB, image/png)
2021-07-01 11:29 UTC, Fushan Wen
Details

Note You need to log in before you can comment on or make changes to this bug.
Description nyanpasu64 2021-06-25 03:31:01 UTC
Not sure if this is a GTK4 bug or a WM bug. For more information, see my GTK4 bug report at https://gitlab.gnome.org/GNOME/gtk/-/issues/4070.

SUMMARY
When GTK4 applications spawn child windows or dialogs, they randomly appear at the top-left corner of the screen instead of on top of their parent.

STEPS TO REPRODUCE
0. Install KDE Plasma on X11, and GTK4.
1. Launch KDE Plasma X11.
2. Run gtk4-demo. Scroll down to "Dialogs" and press "Run" in the title bar.
3. Push "Message Dialog" or "Interactive Dialog".

OBSERVED RESULT
On KDE Plasma X11, with gtk4-demo, the "Dialogs" window has a 45%-ish chance of appearing at the top-left corner of the screen, a 45%-ish chance of appearing where there's empty room on the desktop, and a 10%-ish chance of appearing on top of the parent window. The "Message Dialog" (with title bar) and "Interactive Dialog" it creates have a 50%-ish chance of appearing at the top left corner of the screen, instead of on the window. If you `killall kwin_x11` before opening the windows/dialogs, they will always appear in the top-left of the screen.

If you follow the same steps with gtk3-demo, the "Dialogs and Message Boxes" window always appears where there is room on-screen (otherwise on the top-left corner), and the Message/Interactive Dialogs always appear on top of their parent windows. With kwin_x11 killed, all windows and dialogs appear in the top-left corner of the screen.

EXPECTED RESULT
I expect GTK4 windows to not be positioned nondeterministically on KWin (which could be a KWin bug, or something more complex), and not be placed in the top-left corner of the screen. Ideally GTK4 windows would behave like GTK3 windows.

Does GTK4 think it's the responsibility of the GUI toolkit or WM to position windows? I'm guessing both GTK3 and GTK4 do not communicate window positions to X, but instead supplies instructions to the WM, and GTK4 does so differently, and they're either wrong or misinterpreted by non-GNOME WMs. (I've heard on the gtk-rs Matrix that GTK4 doesn't compute window positions itself, and I observed that gtk3-demos's "Dialogs and Message Boxes" window is placed on the parent in XFCE, but in empty space on KDE.) And there are even bigger behavioral differences on GTK4.

----

I didn't take the time to test gtk3/4-demo's behavior on GNOME or any DE's Wayland, but I did test gtk3/4-widget-factory's behavior. (See https://gitlab.gnome.org/GNOME/gtk/-/issues/4070#additional-information.) Window positioning is much more consistent on Wayland. I suspect that's because the WM, not the toolkit, manages window positioning, so GTK4 has no room to screw positions up. But I don't know why GTK on X11 has inconsistent windowing behavior, since I think it delegates positioning to the WM like Wayland does. (I think so because it spawns windows at (0, 0) without a WM running.)

SOFTWARE/OS VERSIONS
Operating System: Arch Linux
KDE Plasma Version: 5.22.2
KDE Frameworks Version: 5.83.0
Qt Version: 5.15.2
Kernel Version: 5.12.12-zen1-1-zen (64-bit)
Graphics Platform: X11
Processors: 12 × AMD Ryzen 5 5600X 6-Core Processor
Memory: 15.6 GiB of RAM
Graphics Processor: NVIDIA GeForce GT 730/PCIe/SSE2

GTK: 4.2.1

ADDITIONAL INFORMATION
Comment 1 Fushan Wen 2021-07-01 11:29:30 UTC
Created attachment 139780 [details]
There are chances that the dialog window locates in the center of the parent window.

It can also be reproduced on openSUSE Tumbleweed.

Operating System: openSUSE Tumbleweed 20210628
KDE Plasma Version: 5.22.2
KDE Frameworks Version: 5.83.0
Qt Version: 5.15.2
Kernel Version: 5.12.13-1-default (64-bit)
Graphics Platform: X11
Processors: 8 × AMD Ryzen 7 4700U with Radeon Graphics
Memory: 15.0 GiB of RAM
Graphics Processor: AMD RENOIR
Comment 2 nyanpasu64 2021-07-01 11:38:07 UTC
I also noticed that in GTK4 apps, if the dialog spawns on the parent window rather than in the corner of the screen, the KWin title bar will have an empty space to the right of the X button (visible in Comment 1's screenshot).
Comment 3 Fushan Wen 2021-07-02 13:33:02 UTC
Running kwin_x11 using 
env QT_LOGGING_RULES="*.debug=true;qt.qpa*.debug=false" kwin_x11 --replace

If dialog locates in the top left corner:

kwin_core: User timestamp, ASN: 2520815
kwin_core: User timestamp, final: KWin::X11Client(0x56377cf7f630, windowId=0x100017b, caption="Dialogs <2>\u200E") : 2520815
kwin_core: Activation: Belongs to active application
kwin_core: XCB error: 152 (BadDamage), sequence: 13413, resource id: 54529620, major code: 143 (DAMAGE), minor code: 2 (Destroy)


If dialog locates in the center of the parent window:

kwin_core: User timestamp, ASN: 2522656
kwin_core: User timestamp, final: KWin::X11Client(0x56377cf7f630, windowId=0x100018f, caption="Dialogs <2>\u200E", transientFor=KWin::X11Client(0x56377cf089b0, windowId=0x1000004, caption="Dialogs")) : 2522656
kwin_core: Activation: Belongs to active application
kwin_core: XCB error: 152 (BadDamage), sequence: 15137, resource id: 54529671, major code: 143 (DAMAGE), minor code: 2 (Destroy)
Comment 4 Fushan Wen 2021-07-02 14:49:58 UTC
▣The transient status bool is defined here(https://github.com/KDE/kwin/blob/8c8098a61c8990076f6d0866d662ab7980e49dc7/src/x11client.h#L559) The default value of m_transientForId is equal to XCB_WINDOW_NONE.

▣ m_transientForId is modified by setTransient(). (https://github.com/KDE/kwin/blob/c61085dc2e28cb7d737c9b049499b4433916b194/src/x11client.cpp#L2966)

▣setTransient() is called by readTransientProperty()(https://github.com/KDE/kwin/blob/c61085dc2e28cb7d737c9b049499b4433916b194/src/x11client.cpp#L2947). 
1. If transientFor.getTransientFor(&new_transient_for_id) returns false, isTransient() is false.
2. If verifyTransientFor() fails, isTransient() is false. There are some debug messages defined in verifyTransientFor(), but I haven't seen them. So I guess WM_TRANSIENT_FOR is None when verifyTransientFor() fails.

▣verifyTransientFor() is defined here(https://github.com/KDE/kwin/blob/c61085dc2e28cb7d737c9b049499b4433916b194/src/x11client.cpp#L3120)

More debug information is needed to find what part is wrong.
Comment 5 Fushan Wen 2021-07-02 16:40:24 UTC
Top-left corner:
kwin_core: transientFor.getTransientFor() returns False. "Dialogs <2>\u200E"
kwin_core: verifyTransientFor(): new_transient_for == XCB_WINDOW_NONE and set is False. "Dialogs <2>\u200E"
kwin_core: User timestamp, ASN: 13849012
kwin_core: User timestamp, final: KWin::X11Client(0x55822e5d3610, windowId=0x1000096, caption="Dialogs <2>\u200E") : 13849012
kwin_core: Activation: Belongs to active application
kwin_core: transientFor.getTransientFor() returns True. "Dialogs <2>\u200E"
kwin_core: setTransient(): new_transient_for_id != m_transientForId is True. "Dialogs <2>\u200E"
kwin_core: setTransient(): m_transientForId != XCB_WINDOW_NONE && !groupTransient() is True. "Dialogs <2>\u200E"


Center:
kwin_core: transientFor.getTransientFor() returns True. "Dialogs <2>\u200E"
kwin_core: setTransient(): new_transient_for_id != m_transientForId is True. "Dialogs <2>\u200E"
kwin_core: setTransient(): m_transientForId != XCB_WINDOW_NONE && !groupTransient() is True. "Dialogs <2>\u200E"
kwin_core: User timestamp, ASN: 13970306
kwin_core: User timestamp, final: KWin::X11Client(0x55822e5d3610, windowId=0x10000b8, caption="Dialogs <2>\u200E", transientFor=KWin::X11Client(0x55822dd21fa0, windowId=0x1000004, caption="Dialogs")) : 13970306
kwin_core: Activation: Belongs to active application

Problem exists in transientFor.getTransientFor()
Comment 6 nyanpasu64 2021-07-04 04:59:30 UTC
https://bugs.kde.org/show_bug.cgi?id=439137#add_comment

The upstream maintainers have replied: (https://gitlab.gnome.org/GNOME/gtk/-/issues/4070#note_1197122):

> We set a transient parent, and mark the dialogs as modal.
>
> The rest is up to the wm.

They closed the bug, saying it's not GTK4's problem. However, the transient-ness of child windows randomly fails to be picked up by kwin, and I suspect (based on windows opening at the center of the screen) it always fails to be picked up by xfwm4. I still don't know if xfwm4 and kwin are both broken in the same scenario but with different symptoms, and mutter and openbox are unaffected.

I theorize there's a race condition where GTK4 exposes a new window to the WM before marking it as transient, and the running WM finds a new window and checks the transient state at some point either before or after GTK sets it. xfwm4 always fails to see the transient state, KDE sometimes sees the transient state, and mutter/openbox always see the transient state.

KDE is wonky: If the "Dialogs" window appears "where there's empty room" or "on top of the parent window", or if the "Message Dialog" appears "on the window", KDE only puts 2 buttons in the title bar, but adds a gap to the right for a third button. I suspect this is caused by the same-ish race condition.)

----

I decided to perform more logging of opening the "Dialog" window (not dialog box), since it had 3 different cases rather than 2. I copied the above command line:
env QT_LOGGING_RULES="*.debug=true;qt.qpa*.debug=false" kwin_x11 --replace
(I don't know of any analogous command lines for X11 in general, or other WMs.)

*On GTK4*

"Dialog" window spawns at top left:

kwin_core: User timestamp, ASN: 1248132
kwin_core: User timestamp, final: KWin::X11Client(0x5556e3e0ac90, windowId=0x5000072, caption="Dialogs <2>\u200E", transientFor=KWin::X11Client(0x5556e3b7aed0, windowId=0x5000004, caption="Dialogs")) : 1248132
kwin_core: Activation: Belongs to active application

"Dialog" window spawns in empty space (gap to the right of close button):

kwin_core: User timestamp, ASN: 1555150
kwin_core: User timestamp, final: KWin::X11Client(0x5654aa852520, windowId=0x5000282, caption="Dialogs <2>\u200E") : 1555150
kwin_core: Activation: Belongs to active application

"Dialog" window spawns on top of parent (gap to the right of close button):

kwin_core: User timestamp, ASN: 1669802
kwin_core: User timestamp, final: KWin::X11Client(0x5654aa85c6a0, windowId=0x50002fe, caption="Dialogs <2>\u200E", transientFor=KWin::X11Client(0x5654aa257e40, windowId=0x5000004, caption="Dialogs")) : 1669802
kwin_core: Activation: Belongs to active application

*On GTK3*

"Dialog and Message Boxes" window always ends up in empty space (no gap to the right of close button):

kwin_core: User timestamp, ASN: 1602765
kwin_core: User timestamp, final: KWin::X11Client(0x5654aa6e0af0, windowId=0x6000bcf, caption="Dialogs and Message Boxes <2>\u200E", transientFor=KWin::X11Client(0x5654aa6e2fa0, windowId=0x6000007, caption="Dialogs and Message Boxes")) : 1602765
kwin_core: Activation: Belongs to active application

*Analysis*

On GTK4, if I spawn the "Dialogs" window, top left" and "on top of parent" both show transientFor in the debug message. "In empty space" does not.

If I spawn "Message Dialog", both "top left" and "on top of parent" show transientFor in the debug message, and I've never seen a dialog appear without a transientFor message, regardless if it's positioned properly or not. This differs from what qydwhotmail saw!

On GTK3, if I spawn the "Dialogs" window, it always spawns on top of parent and always shows transientFor.

*Sidenote*

After I alt-tab slowly enough for the sidebar to actually appear, then KWin's debug logging gets a lot noisier permanently until I restart kwin. For example:

qt.scenegraph.renderloop: - animationStarted()
qt.scenegraph.renderloop: *** Starting animation timer
qt.scenegraph.renderloop: - polish and sync update request
qt.scenegraph.renderloop: polishAndSync (normal) PlasmaQuick::Dialog(0x55db957c0970, visibility=QWindow::Hidden, flags=QFlags<Qt::WindowType>(Dialog|X11BypassWindowManagerHint|FramelessWindowHint|WindowMinMaxButtonsHint), geometry=0,0 396x1440)
qt.scenegraph.renderloop: - not exposed, abort
qt.scenegraph.renderloop: - ticking non-visual timer
qt.scenegraph.renderloop: - ticking non-visual timer
qt.scenegraph.renderloop: - ticking non-visual timer
kwin_core: User timestamp, ASN: 904915
kwin_core: User timestamp, final: KWin::X11Client(0x55db95dca8f0, windowId=0x3a00326, caption="Dialogs <2>\u200E") : 904915
kwin_core: Activation: Belongs to active application
Comment 7 Martin Flöser 2021-07-04 05:24:09 UTC
From the description it sounds that window gets mapped without the WM_TRANSIENT_FOR property and KWin and likewise XFWM does not support changing the state of that property.

I just checked ICCCM and could not find anything regarding whether it is allowed or disallowed to change that property once the window is mapped. In my opinion it doesn't make sense to change it once it is mapped. At least I cannot imagine how a window manager should handle this property change once the window is mapped.

If you are able to monitor the property states once the window gets mapped and then monitor for changes of the property we know more.
Comment 8 nyanpasu64 2021-07-04 06:11:12 UTC
On the technical side of things, how would I "monitor the property states once the window gets mapped and then monitor for changes of the property"? Should I insert debug statements into kwin? Or try reading kwin or GTK4 myself?

On the social side of things, should I ping the GTK bug and ask them to reevaluate GTK4's code? Should I mention my hypothesis or your message?
Comment 9 Martin Flöser 2021-07-04 06:24:24 UTC
(In reply to nyanpasu64 from comment #8)
> On the technical side of things, how would I "monitor the property states
> once the window gets mapped and then monitor for changes of the property"?
> Should I insert debug statements into kwin? Or try reading kwin or GTK4
> myself?

Yes adding debug statements in KWin is the way I would go. It might be possible to also use KWin scripts, though I am not completely sure whether that is exposed (especially if KWin does not handle transient_for changes the change signal is probably missing).

> 
> On the social side of things, should I ping the GTK bug and ask them to
> reevaluate GTK4's code? Should I mention my hypothesis or your message?

I would wait till we have more information. If the hypothesis is correct then we should ask GTK to reevaluate the code.
Comment 10 Fushan Wen 2021-07-04 16:23:39 UTC
I guess the window does change its WM_TRANSIENT_FOR property after its initialization if I add debug messages to the right places.

I use a custom patch to add more debug information to KWin.(https://build.opensuse.org/package/view_file/home:fusionfuture:branches:KDE:Frameworks5/kwin5/enable-debug.patch?expand=1)

Below are debug logs.
=============Top-left START=============
kwin_core: fetchTransient(): window() = 8388672
kwin_core: manage(): calling readTransientProperty(transientCookie)
kwin_core: readTransientProperty(): transientFor.getTransientFor() returns False. "Dialogs <2>\u200E"
kwin_core: verifyTransientFor(): new_transient_for == XCB_WINDOW_NONE and set is False. "Dialogs <2>\u200E"
kwin_core: readTransientProperty(): (2)new_transient_for_id =  0
kwin_core: User timestamp, ASN: 10647473
kwin_core: User timestamp, final: KWin::X11lCient(0x5576a65c3b40, windowId=0x800040, caption="Dialogs <2>\u200E") : 10647473
kwin_core: Activation: Belongs to active application
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: fetchTransient(): window() = 8388672
kwin_core: readTransient(): calling readTransientProperty(transientFor)
kwin_core: readTransientProperty(): (1)new_transient_for_id =  8388612
kwin_core: readTransientProperty(): transientFor.getTransientFor() returns True. "Dialogs <2>\u200E"
kwin_core: readTransientProperty(): (2)new_transient_for_id =  8388612
kwin_core: setTransient(): new_transient_for_id ( 8388612 ) != m_transientForId( 0 ) is True.
kwin_core: setTransient(): m_transientForId != XCB_WINDOW_NONE && !groupTransient() is True. "Dialogs <2>\u200E"
=============Top-left END=============

It can be seen from the log and the source code that readTransient() is called by propertyNotifyEvent(xcb_property_notify_event_t *e), and the case is XCB_ATOM_WM_TRANSIENT_FOR.


=============Center START=============
kwin_core: fetchTransient(): window() = 8388759
kwin_core: manage(): calling readTransientProperty(transientCookie)
kwin_core: readTransientProperty(): (1)new_transient_for_id =  8388612
kwin_core: readTransientProperty(): transientFor.getTransientFor() returns True. "Dialogs <2>\u200E"
kwin_core: readTransientProperty(): (2)new_transient_for_id =  8388612
kwin_core: setTransient(): new_transient_for_id ( 8388612 ) != m_transientForId( 0 ) is True.
kwin_core: setTransient(): m_transientForId != XCB_WINDOW_NONE && !groupTransient() is True. "Dialogs <2>\u200E"
kwin_core: User timestamp, ASN: 10694350
kwin_core: User timestamp, final: KWin::X11Client(0x5576a6f708e0, windowId=0x800097, caption="Dialogs <2>\u200E", transientFor=KWin::X11Client(0x5576a664eed0, windowId=0x800004, caption="Dialogs")) : 10694350
kwin_core: Activation: Belongs to active application
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
kwin_core: checkTransient(): m_originalTransientForId != w
=============Center END=============

Here, readTransient() is not called.
Comment 11 Martin Flöser 2021-07-04 17:30:06 UTC
Good! That explains the behavior. The placement is done in the manage method, thus once we know it's a transient, it's too late.

I think this should go back to GTK devs to look into ensuring that the property only gets update in withdrawn state.
Comment 12 nyanpasu64 2021-07-04 18:29:48 UTC
Who should comment on the GTK issue? I'm not the most well-versed in KWin or the underlying X11 protocol causing this bug. If I were to comment on GTK, I'd say:

"There's a race condition where GTK4 exposes a new window to the WM before marking it as transient, and the running WM finds a new window and checks the transient state at some point either before or after GTK sets it. xfwm4 always fails to see the transient state, KDE sometimes sees the transient state, and mutter/openbox always see the transient state."

And link to this bug's Comment 10 or 11.
Comment 13 Fushan Wen 2021-07-05 02:48:46 UTC
I studied the source code of gtk-demo, and found the bug may exist in the demo program.

1. In main.c, gtk_demo_run() is called when "Run" button is pressed. (https://gitlab.gnome.org/GNOME/gtk/-/blob/master/demos/gtk-demo/main.c#L267)

2, In gtk_demo_run(), "self->func (window)" is called BEFORE gtk_window_set_transient_for() is called which sets the transient property. (https://gitlab.gnome.org/GNOME/gtk/-/blob/master/demos/gtk-demo/main.c#L155)

3. With regard to "self->func (window)", "window" refers to the parent window, and "func" refers to "do_dialog" in dialog.c

4. do_dialog() calls gtk_widget_show() after attributes of the widget are set. (https://gitlab.gnome.org/GNOME/gtk/-/blob/master/demos/gtk-demo/dialog.c#L187)


That should explain the race condition. do_dialog() is called before gtk_window_set_transient_for(), and I don't know why they intend to do that. But in message_dialog_clicked(), gtk_window_set_transient_for() called by gtk_message_dialog_new() is called before gtk_widget_show(). (https://gitlab.gnome.org/GNOME/gtk/-/blob/master/demos/gtk-demo/dialog.c#L22)
(https://gitlab.gnome.org/GNOME/gtk/-/blob/master/gtk/gtkmessagedialog.c#L532)
Comment 14 Fushan Wen 2021-07-05 03:06:47 UTC
An issue about gtk-demo is also reported.

https://gitlab.gnome.org/GNOME/gtk/-/issues/4090
Comment 15 nyanpasu64 2021-07-05 05:41:22 UTC
I don't think the bug is solely in the demo program. gtk4-widget-factory and gtk4-rs apps are also affected, and gtk3-demo (organized differently from gtk4-demo), gtk3-widget-factory, and gtk3-rs apps are unaffected. I might write a minimal C demo program for gtk3 and gtk4 and see if I can get the same-ish code to behave differently.

This bug in window creation order may be related to how gtk4 removed the gtk_widget_show_all function. I suspected that gtk4 apps show the window before gtk3 would, but https://docs.gtk.org/gtk4/migrating-3to4.html#widgets-are-now-visible-by-default says that GTK4 windows and dialogs and such must be explicitly shown, so I'm not sure.
Comment 16 nyanpasu64 2021-07-07 01:57:41 UTC
FYI there is more investigation and discussion happening at https://gitlab.gnome.org/GNOME/gtk/-/issues/4090. I think the buggy GTK4 behavior is not caused by the "dialog parent" race condition, but rather because KWin sees/shows the window before GTK4 renders the window contents and configures the title bar buttons.

Is it also a KWin bug that it handles GTK4's behavior poorly and draws the wrong title bar button layout?
Comment 17 Nate Graham 2021-07-30 17:01:36 UTC
Looks like a GTK bug I guess!
Comment 18 nyanpasu64 2021-07-30 17:04:09 UTC
Sadly the GTK folks don't think GTK4 is doing anything wrong, and I stopped investigating, so I still don't know what the issue is.