toga icon indicating copy to clipboard operation
toga copied to clipboard

`window.size` setter doesn't work when called after `window.show()` in `startup()` method on `gtk`

Open proneon267 opened this issue 1 year ago • 3 comments

Describe the bug

window.size setter doesn't work when called after window.show() in startup() method on gtk, but works when called before window.show() method.

Steps to reproduce

  1. Use the latest toga main branch
  2. Install with:
pip install -e ./core -e ./dummy -e ./gtk
  1. Generate a new project with briefcase
  2. Replace the app.py script with the following:
"""
My first application
"""
import toga
from toga.style import Pack
from toga.style.pack import COLUMN, ROW


class HelloWorld(toga.App):

    def startup(self):
        """
        Construct and show the Toga application.

        Usually, you would add your application to a main content box.
        We then create a main window (with a name matching the app), and
        show the main window.
        """
        main_box = toga.Box()

        self.main_window = toga.MainWindow(title=self.formal_name)
        self.main_window.content = main_box
        
        
        self.main_window.show()

        self.main_window.size = (200,100)
        print(self.main_window.size)
        assert self.main_window.size == (200,100)


def main():
    return HelloWorld()
  1. Errors will be encountered and returns the default window size value:
[helloworld] Starting in dev mode...
===========================================================================
(640, 480)
Traceback (most recent call last):
  File "/home/proneon267/venv/lib/python3.11/site-packages/toga_gtk/app.py", line 87, in gtk_startup
    self.interface._startup()
  File "/home/proneon267/venv/lib/python3.11/site-packages/toga/app.py", line 710, in _startup
    self.startup()
  File "/home/proneon267/helloworld/src/helloworld/app.py", line 29, in startup
    assert self.main_window.size == (200,100)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError
  1. Replace app.py with the following:
"""
My first application
"""
import toga
from toga.style import Pack
from toga.style.pack import COLUMN, ROW


class HelloWorld(toga.App):

    def startup(self):
        """
        Construct and show the Toga application.

        Usually, you would add your application to a main content box.
        We then create a main window (with a name matching the app), and
        show the main window.
        """
        main_box = toga.Box()

        self.main_window = toga.MainWindow(title=self.formal_name)
        self.main_window.content = main_box

        self.main_window.size = (200,100)
        print(self.main_window.size)
        assert self.main_window.size == (200,100)
        
        
        self.main_window.show()


def main():
    return HelloWorld()
  1. No errors will be encountered and will return the correct value of self.main_window.size:
[helloworld] Starting in dev mode...
===========================================================================
(200, 100)

Expected behavior

window.size should return the correct value in both the cases.

I guess this error is related to the call: https://github.com/beeware/toga/blob/f4dee4fdf6b1f3fb5639a12f639323afb8151f2a/gtk/src/toga_gtk/window.py#L21

Screenshots

No response

Environment

  • Operating System: Arch
  • Python version: 3.11.6
  • Software versions:
    • Briefcase: 0.3.16
    • Toga: latest
    • ...

Logs

No logs were generated

Additional context

No response

proneon267 avatar Jan 28 '24 17:01 proneon267

While this is definitely an odd inconsistency, I don't believe we should consider it a bug - or, at least, to the extent that it's a bug, the behaviour will be highly platform dependent, and almost impossible to fix, and we should document our way out of needing to fix it.

The issue is that invoking a background API doesn't necessarily mean that the effect of that API has been immediately applied. In this case, you're showing a window, then changing the window size, then inspecting the window size - but at no point are you releasing control to the GUI event loop to process the size change. Retrieving the window size is done against the "actual" size - the current manifested size of the window; but because the size change hasn't been actually drawn, you end up in a situation where you're asking for the size of the window before the drawing instruction to change the window size hasn't been applied yet.

The problem isn't an issue when the change occurs before the show(), because in that instance there's no GUI instruction required, so it's immediately applied internally by GTK. This is an internal implementation detail, though.

This problem doesn't exist on every platform - windows, macOS and iOS, for example, apply changes immediately. However, we've had multiple reports about analogous problems on Android (see #2274, 2281).

Unfortunately, I can't think of any obvious way to work around this problem, though, short of putting a "process all outstanding events" call before every attribute retrieval on GTK. I strongly suspect that this particular cure would be worse than the disease.

Ultimately, I suspect the actual "fix" for this will be documentation - highlighting that properties aren't necessarily reflected immediately.

The good news is that I don't think that this is that big of a problem. In practice, I can't think of many use cases that involve window.size = (x, y); if window.size > ... type logic - you're either setting a new size, or you're responding to a new size... not both in the same "context". I suspect that you're only observing this problem because you're messing around with testing resize behavior - and testing like this is the one area where the problem manifests a lot. That's why the tests contain a lot of await probe.redraw() calls - specifically to give the GUI the opportunity to reflect any property changes.

freakboy3742 avatar Jan 29 '24 02:01 freakboy3742

I agree that it should be a documentation fix. But, can you take a look at the on_resize PR's test: https://github.com/beeware/toga/actions/runs/7696871621/job/20972747241?pr=2364

=================================== FAILURES ===================================
________________________________ test_on_resize ________________________________
Traceback (most recent call last):
  File "/home/runner/work/toga/toga/testbed/build/testbed/ubuntu/jammy/testbed-0.0.1/usr/lib/testbed/app/tests/test_window.py", line 496, in test_on_resize
    assert main_window.size == (200, 150)
AssertionError: assert (288, 150) == (200, 150)
  At index 0 diff: 288 != 200
  Full diff:
  - (200, 150)
  ?   ^^
  + (288, 150)
  ?   ^^

I am using wait_for_window() methods, but I am not sure, why main_window.size is returning the wrong size. The same error occurs locally.

proneon267 avatar Jan 29 '24 13:01 proneon267

My guess is that you're hitting your operating system minimum size. Taking things to the extreme - you can't make a window 10x10. At some point, there's a minimum size that allows the window manager to display buttons, window titles etc. It appears that 288 pixels is the lower limit for the horizontal size of a window in your test environment. This value is highly platform dependent - it depends on the window manager that you're using; so for test purposes, make sure you pick a number that is "big enough" to be safe, while still testing whatever property you're asserting.

freakboy3742 avatar Jan 29 '24 23:01 freakboy3742