Skip to content
Open
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
bbceb8c
Added on_resize handler on oga.Window
proneon267 Jan 26, 2024
9429020
Added changelog
proneon267 Jan 26, 2024
cd8abe0
Fixed test
proneon267 Jan 26, 2024
b80483b
Added on textual backend
proneon267 Jan 26, 2024
55a8f4b
Updated test
proneon267 Jan 26, 2024
9353148
Fixed tests
Jan 28, 2024
5429987
Miscellaneous Fixes
proneon267 Jan 28, 2024
8d131cd
Misc. Fix
proneon267 Jan 29, 2024
96ddd64
Merge branch 'main' into on_resize_handler
proneon267 Mar 21, 2024
3291ae1
updated to latest main branch
proneon267 Mar 21, 2024
0b24cc8
Remove PR changes
proneon267 Jan 26, 2025
702c1db
Merge branch 'main' into pr/proneon267/2364
proneon267 Jan 26, 2025
b494da2
Add back PR changes
proneon267 Jan 26, 2025
8edded7
Add test
proneon267 Jan 26, 2025
bf8b9ed
Cleanup
proneon267 Jan 26, 2025
857ff3e
Cleanup
proneon267 Jan 26, 2025
3d8d952
Merge branch 'main' into on_resize_handler
proneon267 Jan 28, 2025
38999e3
Add new test
proneon267 Jan 28, 2025
c2d5ab5
Restart CI
proneon267 Jan 28, 2025
e10f729
Merge branch 'main' into on_resize_handler
proneon267 Feb 5, 2025
a1c9b53
Detect resize on gtk4
proneon267 Feb 5, 2025
522798a
Try detecting window resize on gtk4
proneon267 Feb 5, 2025
a5d7e12
Skip on unsupported platforms
proneon267 Feb 5, 2025
877b736
Implement on web backend
proneon267 Feb 5, 2025
b8672e2
Trigger on_resize when entering presentation
proneon267 Feb 5, 2025
0091aed
Fix `on_resize()` triggering on winforms
proneon267 Feb 5, 2025
c17b1ec
Fix gtk implementation
proneon267 Feb 5, 2025
81be13a
Fix test
proneon267 Feb 5, 2025
38bf570
Apply suggestions from review
proneon267 Feb 7, 2025
7e98675
Skip on gtk4
proneon267 Feb 7, 2025
fc59fe4
Change order of `on_resize()` triggering
proneon267 Feb 7, 2025
33a21e8
Try fix on gtk4 CI
proneon267 Feb 8, 2025
a779463
Use `do_size_allocate()` for GTK4
proneon267 Feb 10, 2025
071d039
Merge branch 'beeware:main' into on_resize_handler
proneon267 Feb 10, 2025
fd58fcc
Fix crash on GTK4
proneon267 Feb 10, 2025
7733a31
Skip on gtk3
proneon267 Feb 10, 2025
d6cec42
Use a gtk vfunc delegate to reduce code duplication
proneon267 Feb 11, 2025
aef5b8d
Merge branch 'on_resize_handler' of https://github.com/proneon267/tog…
proneon267 Feb 11, 2025
6fe1167
Enable GTK4 virtual functions to be overridden manually
proneon267 Feb 13, 2025
aa54e5b
Remove debugging leftover
proneon267 Feb 13, 2025
eb5c3cb
Skip on GTK3
proneon267 Feb 13, 2025
9adea65
Apply suggestions from review
proneon267 Feb 21, 2025
6c01b60
Merge branch 'beeware:main' into on_resize_handler
proneon267 Feb 21, 2025
2759ffb
Apply suggestions from review
proneon267 Feb 21, 2025
24be047
Apply suggestions from review
proneon267 Feb 21, 2025
8fa656d
Apply suggestions from review
proneon267 Feb 21, 2025
7509eca
Apply suggestions from review
proneon267 Feb 22, 2025
6479d74
Apply suggestions from review
proneon267 Feb 22, 2025
f16f18f
Merge branch 'main' into on_resize_handler
proneon267 Jul 5, 2025
c39854a
Fix Merge Conflicts
proneon267 Jul 5, 2025
ebe2e67
Retrieve VFuncInfo instead of bound method
proneon267 Jul 5, 2025
1ac2e6e
Modify tests according to review
proneon267 Jul 6, 2025
2d35ceb
Remove unneeded test
proneon267 Jul 6, 2025
cbda777
Restart CI for dom storage intermittent failure
proneon267 Jul 6, 2025
393f73f
Merge branch 'beeware:main' into on_resize_handler
proneon267 Sep 4, 2025
07daff0
Apply suggestions from code review
proneon267 Sep 4, 2025
98048f6
Reorder gtk native size allocation call
proneon267 Sep 6, 2025
a86cd65
Correctly infer size while in presentation mode on macOS
proneon267 Sep 16, 2025
1d3bc3b
Correctly infer window size when minimized on winforms
proneon267 Sep 16, 2025
5a99825
Re: Correctly infer window size when minimized on winforms
proneon267 Sep 16, 2025
1516450
Add test to check on_resize handler is called with the correct window…
proneon267 Sep 16, 2025
6ad56d6
correct assertion
proneon267 Sep 17, 2025
2e4bfba
pre-commit correction
proneon267 Sep 17, 2025
b796fe3
Correct on_resize triggering for fullscreen on macOS
proneon267 Sep 17, 2025
e2baade
pre-commit correction
proneon267 Sep 17, 2025
3a15780
use instantaneous state
proneon267 Sep 18, 2025
c932b65
Correct test on macOS-86 & gtk4-wayland
proneon267 Sep 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions android/src/toga_android/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def onGlobalLayout(self):
"""
native_parent = self.window.native_content.getParent()
self.window.resize_content(native_parent.getWidth(), native_parent.getHeight())
self.window.interface.on_resize()


class Window(Container):
Expand Down
1 change: 1 addition & 0 deletions changes/2304.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Toga Windows now supports calling user functions on resize events.
13 changes: 11 additions & 2 deletions cocoa/src/toga_cocoa/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def windowWillClose_(self, notification) -> None:

@objc_method
def windowDidResize_(self, notification) -> None:
self.impl.interface.on_resize()
if self.interface.content:
# Set the window to the new size
self.interface.content.refresh()
Expand Down Expand Up @@ -454,10 +455,14 @@ def _apply_state(self, target_state):
)

# Going presentation mode causes the window content
# to be re-homed in a NSFullScreenWindow; teach the
# new parent window about its Toga representations.
# to be re-homed in a NSFullScreenWindow;
# Teach the new parent window about its Toga representations.
self.container.native.window._impl = self
self.container.native.window.interface = self.interface
# Manually trigger the resize event as the original NSWindow's
# size remains unchanged, hence the windowDidResize_ would not
# be notified when the window goes into presentation mode.
self.interface.on_resize()
self.interface.content.refresh()

# No need to check for other pending states,
Expand All @@ -481,6 +486,10 @@ def _apply_state(self, target_state):
NSNumber.numberWithBool(True), forKey="NSFullScreenModeAllScreens"
)
self.container.native.exitFullScreenModeWithOptions(opts)
# Manually trigger the resize event as the original NSWindow's
# size remains unchanged, hence the windowDidResize_ would not
# be notified when the window goes out of the presentation mode.
self.interface.on_resize()
self.interface.content.refresh()

self.interface.screen = self._before_presentation_mode_screen
Expand Down
47 changes: 40 additions & 7 deletions core/src/toga/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,25 @@ def __call__(self, window: Window, **kwargs: Any) -> None:
...


class OnResizeHandler(Protocol):
def __call__(self, window: Window, **kwargs: Any) -> None:
"""A handler to invoke when a window resizes.

This event is also triggered when any change in available layout size occurs.
However, a change in visibility (e.g. when a window is hidden or minimized)
does not cause a change in layout size and therefore, the event will not be
triggered.

On mobile platforms, it is also triggered when the orientation of the
device is changed.

:param window: The window instance that resizes.
:param kwargs: Ensures compatibility with additional arguments introduced in
future ver
"""
...


_DialogResultT = TypeVar("_DialogResultT")


Expand Down Expand Up @@ -197,6 +216,7 @@ def __init__(
on_lose_focus: OnLoseFocusHandler | None = None,
on_show: OnShowHandler | None = None,
on_hide: OnHideHandler | None = None,
on_resize: OnResizeHandler | None = None,
content: Widget | None = None,
) -> None:
"""Create a new Window.
Expand Down Expand Up @@ -226,6 +246,17 @@ def __init__(
self._closable = closable
self._minimizable = minimizable

# Prime up the event handlers on the interface, as they might be
# called during the initialization of the native window class.
Comment on lines +250 to +251
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On re-review, this raised a red flag for me. Handlers should not be firing as a result of initial conditions passed to the constructor.

If the issue is that the code might result in the handler being invoked, and the handler need to exist, then the priming should be with a None value, with the "live" handler installed later. That ensures that self.interface.on_something() can work, but won't fire an actual handler until construction is complete.

self.on_close = on_close

self.on_gain_focus = on_gain_focus
self.on_lose_focus = on_lose_focus
self.on_show = on_show
self.on_hide = on_hide

self.on_resize = on_resize

# The app needs to exist before windows are created. _app will only be None
# until the window is added to the app below.
self._app: App = None
Expand All @@ -247,13 +278,6 @@ def __init__(
if content:
self.content = content

self.on_close = on_close

self.on_gain_focus = on_gain_focus
self.on_lose_focus = on_lose_focus
self.on_show = on_show
self.on_hide = on_hide

def __lt__(self, other: Window) -> bool:
return self.id < other.id

Expand Down Expand Up @@ -652,6 +676,15 @@ def on_hide(self) -> callable:
def on_hide(self, handler):
self._on_hide = wrapped_handler(self, handler)

@property
def on_resize(self) -> OnResizeHandler:
"""The handler to invoke when the window resizes."""
return self._on_resize

@on_resize.setter
def on_resize(self, handler):
self._on_resize = wrapped_handler(self, handler)

######################################################################
# 2024-06: Backwards compatibility for <= 0.4.5
######################################################################
Expand Down
29 changes: 29 additions & 0 deletions core/tests/window/test_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,35 @@ def test_visibility_events(window):
assert_window_on_show(window)


def test_resize_event(window):
"""The window can trigger on_resize() event handler, when the window
size is changed."""
window.show()
assert window.on_resize._raw is None
window_on_resize_handler = Mock()
window.on_resize = window_on_resize_handler
assert window.on_resize._raw == window_on_resize_handler
initial_size = window.size

# Resize the window, on_resize() will be triggered
window.size = (200, 150)
assert window.size == (200, 150)
window_on_resize_handler.assert_called_with(window)
window_on_resize_handler.reset_mock()

# Resize to initial size, on_resize() will be triggered
window.size = initial_size
assert window.size == initial_size
window_on_resize_handler.assert_called_with(window)
window_on_resize_handler.reset_mock()

# Again request for resizing to initial size, on_resize()
# will not be triggered
window.size = initial_size
assert window.size == initial_size
window_on_resize_handler.assert_not_called()


def test_as_image(window):
"""A window can be captured as an image."""
image = window.as_image()
Expand Down
1 change: 1 addition & 0 deletions dummy/src/toga_dummy/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ def get_size(self) -> Size:
def set_size(self, size):
self._action("set size")
self._size = size
self.interface.on_resize()

######################################################################
# Window position
Expand Down
1 change: 1 addition & 0 deletions gtk/src/toga_gtk/libs/gtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
gi.require_version("Gdk", gtk_version)
gi.require_version("Gtk", gtk_version)

from gi._gi import hook_up_vfunc_implementation # noqa: E402, F401
from gi.events import GLibEventLoopPolicy # noqa: E402, F401
from gi.repository import ( # noqa: E402, F401
Gdk,
Expand Down
11 changes: 11 additions & 0 deletions gtk/src/toga_gtk/libs/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,14 @@ def gtk_text_align(alignment):
CENTER: (0.5, Gtk.Justification.CENTER),
JUSTIFY: (0.0, Gtk.Justification.FILL),
}[alignment]


def create_toga_native(native_gtk_class): # pragma: no-cover-if-gtk3
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As noted elsewhere - if this is code that isn't needed on GTK4, it should be in an if GTK_VERSION... block.

Copy link
Contributor Author

@proneon267 proneon267 Feb 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I have moved it into an if block.

"""Create a new native class from a native gtk class, whose virtual functions
could be safely overridden."""
toga_native_class = type(
native_gtk_class.__gtype__.name,
(native_gtk_class,),
{"base_class": native_gtk_class}, # Store the base class type
)
return toga_native_class
22 changes: 22 additions & 0 deletions gtk/src/toga_gtk/libs/wrapper.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this need another module? Isn't it just another utility? It's in wrapper for winforms because there is not "utils" class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I have moved the wrapper class to utils.

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import weakref


class WeakrefCallable: # pragma: no-cover-if-gtk3
"""
A wrapper for callable that holds a weak reference to it.

This can be useful in particular when setting gtk virtual function handlers,
to avoid cyclical reference cycles between python and gi that are detected
neither by the python garbage collector nor the gi.
"""

def __init__(self, function):
try:
self.ref = weakref.WeakMethod(function)
except TypeError: # pragma: no cover
self.ref = weakref.ref(function)

def __call__(self, *args, **kwargs):
function = self.ref()
if function: # pragma: no branch
return function(*args, **kwargs)
46 changes: 40 additions & 6 deletions gtk/src/toga_gtk/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@
from toga.window import _initial_position

from .container import TogaContainer
from .libs import GTK_VERSION, IS_WAYLAND, Gdk, GLib, Gtk
from .libs import GTK_VERSION, IS_WAYLAND, Gdk, GLib, Gtk, hook_up_vfunc_implementation
from .libs.utils import create_toga_native
from .screens import Screen as ScreenImpl

if TYPE_CHECKING: # pragma: no cover
from toga.types import PositionT, SizeT

from .libs.wrapper import WeakrefCallable


class Window:
def __init__(self, interface, title, position, size):
Expand All @@ -23,6 +26,9 @@ def __init__(self, interface, title, position, size):

self.layout = None

if GTK_VERSION >= (4, 0, 0): # pragma: no-cover-if-gtk3
self._window_size = Size(0, 0)

self.create()
self.native._impl = self

Expand All @@ -42,10 +48,17 @@ def __init__(self, interface, title, position, size):
self.native.connect("window-state-event", self.gtk_window_state_event)
self.native.connect("focus-in-event", self.gtk_focus_in_event)
self.native.connect("focus-out-event", self.gtk_focus_out_event)
self.native.connect("configure-event", self.gtk_configure_event)
else: # pragma: no-cover-if-gtk3
self.native.connect("notify::fullscreened", self.gtk_window_state_event)
self.native.connect("notify::maximized", self.gtk_window_state_event)
self.native.connect("notify::minimized", self.gtk_window_state_event)
# do_size_allocate is a virtual function, used to track window resize.
hook_up_vfunc_implementation(
self.native.do_size_allocate,
self.native.__gtype__,
WeakrefCallable(self.gtk_do_size_allocate),
)

self._window_state_flags = None
self._in_presentation = False
Expand Down Expand Up @@ -82,11 +95,31 @@ def __init__(self, interface, title, position, size):
self.native.set_child(self.layout)

def create(self):
self.native = Gtk.Window()
if GTK_VERSION < (4, 0, 0): # pragma: no-cover-if-gtk4
self.native = Gtk.Window()
else: # pragma: no-cover-if-gtk3
self.native = create_toga_native(Gtk.Window)()

######################################################################
# Native event handlers
######################################################################
def gtk_do_size_allocate(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we're adding code that has different code for GTK3/GTK4, we're putting it into if GTK_VERSION ... blocks. This means in future it's clear what code is GTK3 specific; so when the eventual "remove GTK3 support" PR happens, we know exactly what code we can delete.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for clarifying it. I have moved GTK4 code into if blocks.

self, native, width, height, baseline
): # pragma: no-cover-if-gtk3

# Note: Virtual methods can't use super() to access the original
# implementation, so they use native.base_class instead.

# Call the parent class's size_allocate via native.base_class.
native.base_class.do_size_allocate(native, width, height, baseline)

if self._window_size != (width, height):
self._window_size = Size(width, height)
self.interface.on_resize()

def gtk_configure_event(self, widget, data): # pragma: no-cover-if-gtk4
self.interface.on_resize()

def gtk_show(self, widget):
self.interface.on_show()

Expand Down Expand Up @@ -228,12 +261,10 @@ def set_content(self, widget):

def get_size(self) -> Size:
if GTK_VERSION < (4, 0, 0): # pragma: no-cover-if-gtk4
width, height = self.native.get_default_size()
size = self.native.get_size()
return Size(size.width, size.height)
else: # pragma: no-cover-if-gtk3
width, height = self.native.get_default_size()
return Size(width, height)
return self._window_size
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies if I'm forgetting this from a past review - but why is this change required? It seems less than ideal to have window size be a stateful property, rather than something determined at runtime.

Copy link
Contributor Author

@proneon267 proneon267 Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GtkWindow.get_default_size() returns incorrect size(i.e., not the current nor even the previous size), the correct size can only be determined at following the events: gtk_configure_event on GTK3 and do_size_allocate virtual signal on GTK4. Hence, we had cached it at the appropriate event and signal.


def set_size(self, size: SizeT):
if GTK_VERSION < (4, 0, 0): # pragma: no-cover-if-gtk4
Expand Down Expand Up @@ -444,7 +475,10 @@ def get_image_data(self):

class MainWindow(Window):
def create(self):
self.native = Gtk.ApplicationWindow()
if GTK_VERSION < (4, 0, 0): # pragma: no-cover-if-gtk4
self.native = Gtk.ApplicationWindow()
else: # pragma: no-cover-if-gtk3
self.native = create_toga_native(Gtk.ApplicationWindow)()
if GTK_VERSION < (4, 0, 0): # pragma: no-cover-if-gtk4
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've got 2 GTK_VERSION blocks in rapid succession here - we don't need both.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I have merged them.

self.native.set_role("MainWindow")

Expand Down
1 change: 1 addition & 0 deletions iOS/src/toga_iOS/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def application_didChangeStatusBarOrientation_(
"""This callback is invoked when rotating the device from landscape to portrait
and vice versa."""
App.app.interface.main_window.content.refresh()
App.app.interface.current_window.on_resize()


class App:
Expand Down
Loading