Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cascading windows #2620

Merged
merged 9 commits into from
Jun 12, 2024
1 change: 1 addition & 0 deletions changes/2023.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The initial position of each newly created window is now different, cascading down the screen as windows are created.
3 changes: 2 additions & 1 deletion cocoa/src/toga_cocoa/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from toga.command import Command, Separator
from toga.types import Position, Size
from toga.window import _initial_position
from toga_cocoa.container import Container
from toga_cocoa.libs import (
SEL,
Expand Down Expand Up @@ -161,7 +162,7 @@ def __init__(self, interface, title, position, size):

self.set_title(title)
self.set_size(size)
self.set_position(position)
self.set_position(position if position is not None else _initial_position())

self.native.delegate = self.native

Expand Down
2 changes: 1 addition & 1 deletion core/src/toga/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ def __init__(
self,
id: str | None = None,
title: str | None = None,
position: PositionT = Position(100, 100),
position: PositionT | None = None,
size: SizeT = Size(640, 480),
resizable: bool = True,
minimizable: bool = True,
Expand Down
31 changes: 26 additions & 5 deletions core/src/toga/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,29 @@
from toga.widgets.base import Widget


_window_count = -1


def _initial_position() -> Position:
"""Compute a cascading initial position for platforms that don't have a native
implementation.

This is a stateful method; each time it is invoked, it will yield a new initial
position.

:returns: The position for the new window.
"""
# Each new window created without an explicit position is positioned
# 50px down and to the right from the previous window, with the first
# window positioned at (100, 100). Every 15 windows, move back to a
# y coordinate of 100, and start from 50 pixels further right.
global _window_count
_window_count += 1

pos = 100 + (_window_count % 15) * 50
return Position(pos + (_window_count // 15 * 50), pos)


class FilteredWidgetRegistry:
# A class that exposes a mapping lookup interface, filtered to widgets from a single
# window. The underlying data store is on the app.
Expand Down Expand Up @@ -117,7 +140,7 @@ def __init__(
self,
id: str | None = None,
title: str | None = None,
position: PositionT = Position(100, 100),
position: PositionT | None = None,
size: SizeT = Size(640, 480),
resizable: bool = True,
closable: bool = True,
Expand Down Expand Up @@ -178,13 +201,11 @@ def __init__(
self._minimizable = minimizable

self.factory = get_platform_factory()
position = Position(*position)
size = Size(*size)
self._impl = getattr(self.factory, self._WINDOW_CLASS)(
interface=self,
title=title if title else self._default_title,
position=position,
size=size,
position=None if position is None else Position(*position),
size=Size(*size),
)

# Add the window to the app
Expand Down
3 changes: 2 additions & 1 deletion core/tests/app/test_mainwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ def test_create(app):
assert isinstance(window.id, str)
# Window title is the app title.
assert window.title == "Test App"
assert window.position == (100, 100)
# The app has created a main window, so this will be the second window.
assert window.position == (150, 150)
assert window.size == (640, 480)
assert window.resizable
assert window.closable
Expand Down
6 changes: 5 additions & 1 deletion core/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
import pytest

import toga
from toga import window as toga_window
from toga_dummy.utils import EventLog


@pytest.fixture(autouse=True)
def reset_event_log():
def reset_global_state():
# Clear the testing event log
EventLog.reset()
# Reset the global window count
toga_window._window_count = -1


@pytest.fixture(autouse=True)
Expand Down
51 changes: 48 additions & 3 deletions core/tests/window/test_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ def test_window_created(app):
# We can't know what the ID is, but it must be a string.
assert isinstance(window.id, str)
assert window.title == "Toga"
assert window.position == toga.Position(100, 100)
# The app has created a main window, so this will be the second window.
assert window.position == toga.Position(150, 150)
assert window.size == toga.Size(640, 480)
assert window.resizable
assert window.closable
Expand Down Expand Up @@ -207,6 +208,48 @@ def test_set_position(window):
assert window.position == toga.Position(123, 456)


def test_position_cascade(app):
"""The initial position of windows will cascade."""
windows = [app.main_window]

for i in range(0, 14):
win = toga.Window(title=f"Window {i}")
# The for the first 14 new windows (the app creates the first window)
# the x and y coordinates must be the same
assert win.position[0] == win.position[1]
# The position of the window should cascade down
assert win.position[0] > windows[-1].position[0]
assert win.position[1] > windows[-1].position[1]

windows.append(win)

# The 15th window will come back to the y origin, but shift along the x axis.
win = toga.Window(title=f"Window {i}")
assert win.position[0] > windows[0].position[0]
assert win.position[1] == windows[0].position[1]

windows.append(win)

# Cascade another 15 windows
for i in range(16, 30):
win = toga.Window(title=f"Window {i}")
# The position of the window should cascade down
assert win.position[0] > windows[-1].position[0]
assert win.position[1] > windows[-1].position[1]

# The y coordinate of these windows should be the same
# as 15 windows ago; the x coordinate is shifted right
assert win.position[0] > windows[i - 15].position[0]
assert win.position[1] == windows[i - 15].position[1]

windows.append(win)

# The 30 window will come back to the y origin, but shift along the x axis.
win = toga.Window(title=f"Window {i}")
assert win.position[0] > windows[15].position[0]
assert win.position[1] == windows[15].position[1]


def test_set_size(window):
"""The size of the window can be set."""
window.size = (123, 456)
Expand Down Expand Up @@ -392,9 +435,11 @@ def test_screen(window, app):
# window between the screens.
# `window.screen` will return `Secondary Screen`
assert window.screen == app.screens[1]
assert window.position == toga.Position(100, 100)
# The app has created a main window; the secondary window will be at second
# position.
assert window.position == toga.Position(150, 150)
window.screen = app.screens[0]
assert window.position == toga.Position(1466, 868)
assert window.position == toga.Position(1516, 918)


def test_screen_position(window, app):
Expand Down
3 changes: 2 additions & 1 deletion dummy/src/toga_dummy/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import toga_dummy
from toga.types import Size
from toga.window import _initial_position

from .screens import Screen as ScreenImpl
from .utils import LoggedObject
Expand Down Expand Up @@ -50,7 +51,7 @@ def __init__(self, interface, title, position, size):
self.container = Container()

self.set_title(title)
self.set_position(position)
self.set_position(position if position is not None else _initial_position())
self.set_size(size)

def create_toolbar(self):
Expand Down
3 changes: 2 additions & 1 deletion gtk/src/toga_gtk/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from toga.command import Separator
from toga.types import Position, Size
from toga.window import _initial_position

from .container import TogaContainer
from .libs import Gdk, Gtk
Expand All @@ -30,7 +31,7 @@ def __init__(self, interface, title, position, size):
self.native.set_default_size(size[0], size[1])

self.set_title(title)
self.set_position(position)
self.set_position(position if position is not None else _initial_position())

# Set the window deletable/closable.
self.native.set_deletable(self.interface.closable)
Expand Down
2 changes: 1 addition & 1 deletion testbed/tests/app/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from toga.colors import CORNFLOWERBLUE, FIREBRICK, REBECCAPURPLE
from toga.style.pack import Pack

from ..test_window import window_probe
from ..window.test_window import window_probe


@pytest.fixture
Expand Down
Empty file.
Loading
Loading