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

[widget Audit] toga.OptionContainer #1996

Merged
merged 20 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
932efb0
Update docs and core API for OptionContainer.
freakboy3742 Jun 19, 2023
58bdf2e
Core tests converted to Pytest, with 100% coverage.
freakboy3742 Jun 19, 2023
ec6176d
Cocoa implementation to 100% coverage.
freakboy3742 Jun 20, 2023
f8eb53f
GTK optioncontainer coverage to 100%.
freakboy3742 Jun 20, 2023
fbdf363
Introduce a small explicit delay to work around intermittent test fai…
freakboy3742 Jun 20, 2023
4792170
Merge branch 'audit-splitcontainer' into audit-optioncontainer
freakboy3742 Jun 20, 2023
b6d05f9
Lower the horizontal limit that identifies full width.
freakboy3742 Jun 20, 2023
53abedd
Merge branch 'audit-splitcontainer' into audit-optioncontainer
freakboy3742 Jun 22, 2023
49b81b2
Merge branch 'audit-splitcontainer' into audit-optioncontainer
freakboy3742 Jun 26, 2023
15624b5
Merge branch 'audit-splitcontainer' into audit-optioncontainer
freakboy3742 Jun 26, 2023
00b46fb
Merge branch 'main' into audit-optioncontainer
freakboy3742 Jul 17, 2023
eac7feb
Merge branch 'main' into audit-optioncontainer
freakboy3742 Jul 26, 2023
a46e14c
Merge branch 'main' into audit-optioncontainer
freakboy3742 Jul 26, 2023
7747857
Merge branch 'main' into audit-optioncontainer
freakboy3742 Aug 1, 2023
76e8887
Correct some spelling errors.
freakboy3742 Aug 1, 2023
02f7533
Merge branch 'main' into audit-optioncontainer
freakboy3742 Aug 3, 2023
125ddf2
Documentation fixes
mhsmith Aug 9, 2023
d4b6209
Winforms OptionContainer to 100%
mhsmith Aug 9, 2023
7f222a5
Add implementation of tab_enabled for cocoa.
freakboy3742 Aug 9, 2023
fc22290
Add protection against future removal of a private method.
freakboy3742 Aug 9, 2023
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
5 changes: 5 additions & 0 deletions cocoa/tests_backend/widgets/optioncontainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

class OptionContainerProbe(SimpleProbe):
native_class = NSTabView
disabled_tab_selectable = False

# 2023-06-20: This makes no sense, but here we are. If you render an NSTabView with
# a size constraint of (300, 200), and then ask for the frame size of the native
Expand Down Expand Up @@ -38,3 +39,7 @@ def height(self):

def select_tab(self, index):
self.native.selectTabViewItemAtIndex(index)

def tab_enabled(self, index):
# There appears to be no public method for this.
return self.native.tabViewItemAtIndex(index)._isTabEnabled()
7 changes: 3 additions & 4 deletions core/src/toga/widgets/optioncontainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,11 +223,10 @@ def content(self) -> OptionList:
return self._content

@property
def current_tab(self) -> OptionItem:
"""The currently selected tab of content.
def current_tab(self) -> OptionItem | None:
"""The currently selected tab of content, or ``None`` if there are no tabs.

The getter of this property always returns an ``OptionItem``. The setter also
accepts an ``int`` index, or a ``str`` label.
This property can also be set with an ``int`` index, or a ``str`` label.
"""
index = self._impl.get_current_tab_index()
if index is None:
Expand Down
29 changes: 16 additions & 13 deletions examples/optioncontainer/optioncontainer/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,35 @@

class ExampleOptionContainerApp(toga.App):
def _create_options(self):
label_box0 = toga.Label("This is Box 0", style=Pack(padding=10))
label_box1 = toga.Label("This is Box 1", style=Pack(padding=10))
label_box2 = toga.Label("This is Box 2", style=Pack(padding=10))

box0 = toga.Box(children=[label_box0])
box1 = toga.Box(children=[label_box1])
box2 = toga.Box(children=[label_box2])

self.optioncontainer.content.append("Option 0", box0)
self.optioncontainer.content.append("Option 1", box1)
self.optioncontainer.content.append("Option 2", box2)
self._box_count = 0
for i in range(3):
self.optioncontainer.content.append(*self._create_option())
self._refresh_select()

def _create_option(self):
result = (
f"Option {self._box_count}",
toga.Box(
style=Pack(background_color="cyan", padding=10),
children=[toga.Label(f"This is Box {self._box_count}")],
),
)
self._box_count += 1
return result

def _refresh_select(self):
items = []
for i in range(len(self.optioncontainer.content)):
items.append(str(i))
self.select_option.items = items

def on_add_option(self, button):
self.optioncontainer.add("New Option", toga.Box())
self.optioncontainer.content.append(*self._create_option())
self._refresh_select()

def on_insert_option(self, button):
index = self.optioncontainer.current_tab.index
self.optioncontainer.content.insert(index, "New Option", toga.Box())
self.optioncontainer.content.insert(index, *self._create_option())
self._refresh_select()

def on_enable_option(self, button):
Expand Down
6 changes: 5 additions & 1 deletion gtk/tests_backend/widgets/optioncontainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

class OptionContainerProbe(SimpleProbe):
native_class = Gtk.Notebook
disabled_tab_selectable = False

def repaint_needed(self):
return (
Expand All @@ -14,5 +15,8 @@ def repaint_needed(self):

def select_tab(self, index):
# Can't select a tab that isn't visible.
if self.impl.sub_containers[index].get_visible():
if self.tab_enabled(index):
self.native.set_current_page(index)

def tab_enabled(self, index):
return self.impl.sub_containers[index].get_visible()
23 changes: 16 additions & 7 deletions testbed/tests/widgets/test_optioncontainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,26 +126,33 @@ async def test_enable_tab(widget, probe, on_select_handler):

assert widget.content[0].enabled
assert not widget.content[1].enabled
assert probe.tab_enabled(0)
assert not probe.tab_enabled(1)

# Try to select tab 1
# Try to select a disabled tab
probe.select_tab(1)
await probe.redraw("Tab 1 should still be selected")
await probe.redraw("Try to select tab 2")

if probe.disabled_tab_selectable:
assert widget.current_tab.index == 1
on_select_handler.assert_called_once_with(widget)
widget.current_tab = 0
on_select_handler.reset_mock()
else:
assert widget.current_tab.index == 0
on_select_handler.assert_not_called()

assert widget.current_tab.index == 0
assert widget.content[0].enabled
assert not widget.content[1].enabled

# on_select hasn't been invoked.
on_select_handler.assert_not_called()

# Disable item 1 again, even though it's disabled
widget.content[1].enabled = False
await probe.redraw("Tab 2 should still be disabled")

assert widget.content[0].enabled
assert not widget.content[1].enabled

# Select tab 3, which is index 2 in the widget's contentt; but on platforms
# Select tab 3, which is index 2 in the widget's content; but on platforms
# where disabling a tab means hiding the tab completely, it will be *visual*
# index 1, but content index 2. Make sure the indices are all correct.
widget.current_tab = 2
Expand All @@ -164,6 +171,8 @@ async def test_enable_tab(widget, probe, on_select_handler):

assert widget.content[0].enabled
assert widget.content[1].enabled
assert probe.tab_enabled(0)
assert probe.tab_enabled(1)

# Try to select tab 1
probe.select_tab(1)
Expand Down
25 changes: 15 additions & 10 deletions winforms/src/toga_winforms/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,20 @@
from toga_winforms.libs import Color, Point, Size, SystemColors


class Widget:
class Scalable:
Copy link
Member Author

Choose a reason for hiding this comment

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

Any particular reason for making a subclass for this? It's only ever subclassed by Widget, and it's not used as a basis for isolated testing; I'm not sure I see the benefit of a separate base class for this.

Copy link
Member

Choose a reason for hiding this comment

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

It's also subclassed by Window, and used in the position and size methods. This was required by the OptionContainer tests, because they rely on the window being 640x480 CSS pixels.

def init_scale(self, native):
self.scale = native.CreateGraphics().DpiX / 96

# Convert CSS pixels to native pixels
def scale_in(self, value):
return int(round(value * self.scale))

# Convert native pixels to CSS pixels
def scale_out(self, value):
return int(round(value / self.scale))


class Widget(Scalable):
# In some widgets, attempting to set a background color with any alpha value other
# than 1 raises "System.ArgumentException: Control does not support transparent
# background colors". Those widgets should set this attribute to False.
Expand All @@ -17,7 +30,7 @@ def __init__(self, interface):
self._container = None
self.native = None
self.create()
self.scale = self.native.CreateGraphics().DpiX / 96
self.init_scale(self.native)
self.interface.style.reapply()

@abstractmethod
Expand Down Expand Up @@ -54,14 +67,6 @@ def container(self, container):
def viewport(self):
return self._container

# Convert CSS pixels to native pixels
def scale_in(self, value):
return int(round(value * self.scale))

# Convert native pixels to CSS pixels
def scale_out(self, value):
return int(round(value / self.scale))

def get_tab_index(self):
return self.native.TabIndex

Expand Down
51 changes: 23 additions & 28 deletions winforms/src/toga_winforms/widgets/optioncontainer.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,35 @@
from toga_winforms.container import Container
from toga_winforms.libs import WinForms
from System.Windows.Forms import TabControl, TabPage

from ..container import Container
from .base import Widget


class OptionContainer(Widget):
def create(self):
self.native = WinForms.TabControl()
self.native = TabControl()
self.native.Selected += self.winforms_selected
self.panels = []

def add_content(self, index, text, widget):
widget.viewport = Container(self.native)
widget.frame = self
# Add all children to the content widget.
for child in widget.interface.children:
child._impl.container = widget
page = TabPage(text)
self.native.TabPages.Insert(index, page)

item = WinForms.TabPage()
item.Text = text
panel = Container(page)
self.panels.insert(index, panel)
panel.set_content(widget)

# Enable AutoSize on the container to fill
# the available space in the OptionContainer.
widget.AutoSize = True

item.Controls.Add(widget.native)
if index < self.native.TabPages.Count:
self.native.TabPages.Insert(index, item)
else:
self.native.TabPages.Add(item)
# ClientSize is set correctly for a newly-added tab, but is only updated on
# resize for the selected tab. And when the selection changes, the
# newly-selected tab's ClientSize is not updated until some time after the
# Selected event fires.
self.resize_content(panel)
page.ClientSizeChanged += lambda sender, event: self.resize_content(panel)

def remove_content(self, index):
tab_page = self.native.TabPages[index]
self.native.TabPages.Remove(self.native.TabPages[index])
tab_page.Dispose()
panel = self.panels.pop(index)
panel.clear_content()

def set_on_select(self, handler):
pass
self.native.TabPages.RemoveAt(index)

def set_option_enabled(self, index, enabled):
"""Winforms documentation states that Enabled is not meaningful for this
Expand All @@ -61,7 +55,8 @@ def set_current_tab_index(self, current_tab_index):
self.native.SelectedIndex = current_tab_index

def winforms_selected(self, sender, event):
if self.interface.on_select:
self.interface.on_select(
self.interface, option=self.interface.content[self.native.SelectedIndex]
)
self.interface.on_select(None)

def resize_content(self, panel):
size = panel.native_parent.ClientSize
panel.resize_content(size.Width, size.Height)
11 changes: 3 additions & 8 deletions winforms/src/toga_winforms/widgets/splitcontainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@
from .base import Widget


class SplitPanel(Container):
def resize_content(self, **kwargs):
size = self.native_parent.ClientSize
super().resize_content(size.Width, size.Height, **kwargs)


class SplitContainer(Widget):
def create(self):
self.native = NativeSplitContainer()
Expand All @@ -25,7 +19,7 @@ def create(self):
# (at least on Windows 10), which would make the split bar invisible.
self.native.BorderStyle = BorderStyle.Fixed3D

self.panels = (SplitPanel(self.native.Panel1), SplitPanel(self.native.Panel2))
self.panels = (Container(self.native.Panel1), Container(self.native.Panel2))
self.pending_position = None

def set_bounds(self, x, y, width, height):
Expand Down Expand Up @@ -81,4 +75,5 @@ def get_max_position(self):

def resize_content(self, **kwargs):
for panel in self.panels:
panel.resize_content(**kwargs)
size = panel.native_parent.ClientSize
panel.resize_content(size.Width, size.Height, **kwargs)
14 changes: 9 additions & 5 deletions winforms/src/toga_winforms/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

from .container import Container, MinimumContainer
from .libs import Point, Size, WinForms
from .widgets.base import Scalable


class Window(Container):
class Window(Container, Scalable):
def __init__(self, interface, title, position, size):
self.interface = interface
self.interface._impl = self
Expand All @@ -22,6 +23,7 @@ def __init__(self, interface, title, position, size):
self.native._impl = self
self.native.FormClosing += self.winforms_FormClosing
super().__init__(self.native)
self.init_scale(self.native)

self.native.MinimizeBox = self.native.interface.minimizable

Expand Down Expand Up @@ -73,16 +75,18 @@ def create_toolbar(self):
self.resize_content()

def get_position(self):
return self.native.Location.X, self.native.Location.Y
location = self.native.Location
return tuple(map(self.scale_out, (location.X, location.Y)))

def set_position(self, position):
self.native.Location = Point(*position)
self.native.Location = Point(*map(self.scale_in, position))

def get_size(self):
return self.native.ClientSize.Width, self.native.ClientSize.Height
size = self.native.ClientSize
return tuple(map(self.scale_out, (size.Width, size.Height)))

def set_size(self, size):
self.native.ClientSize = Size(*size)
self.native.ClientSize = Size(*map(self.scale_in, size))

def set_app(self, app):
if app is None:
Expand Down
14 changes: 14 additions & 0 deletions winforms/tests_backend/widgets/optioncontainer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from System.Windows.Forms import TabControl

from .base import SimpleProbe


class OptionContainerProbe(SimpleProbe):
native_class = TabControl
disabled_tab_selectable = True

def select_tab(self, index):
self.native.SelectedIndex = index

def tab_enabled(self, index):
return self.native.TabPages[index].Enabled