From 827f7ee929d809fc2f649ce555c8527550017123 Mon Sep 17 00:00:00 2001 From: Patrik Dufresne Date: Thu, 29 Feb 2024 13:29:22 -0500 Subject: [PATCH 1/2] Fix `visibility` and `display` support in Pack() --- changes/2428.bugfix.rst | 1 + core/src/toga/style/pack.py | 30 +++++++++++++++++-- gtk/src/toga_gtk/widgets/activityindicator.py | 5 +--- gtk/src/toga_gtk/widgets/base.py | 18 +++++++---- gtk/src/toga_gtk/widgets/button.py | 5 +--- gtk/src/toga_gtk/widgets/canvas.py | 1 - gtk/src/toga_gtk/widgets/divider.py | 4 +-- gtk/src/toga_gtk/widgets/label.py | 8 +---- gtk/src/toga_gtk/widgets/numberinput.py | 4 +-- gtk/src/toga_gtk/widgets/progressbar.py | 5 +--- gtk/src/toga_gtk/widgets/selection.py | 3 +- gtk/src/toga_gtk/widgets/slider.py | 3 +- gtk/src/toga_gtk/widgets/switch.py | 8 ++--- gtk/src/toga_gtk/widgets/textinput.py | 8 +---- gtk/src/toga_gtk/window.py | 5 +++- 15 files changed, 56 insertions(+), 52 deletions(-) create mode 100644 changes/2428.bugfix.rst diff --git a/changes/2428.bugfix.rst b/changes/2428.bugfix.rst new file mode 100644 index 0000000000..43a7009869 --- /dev/null +++ b/changes/2428.bugfix.rst @@ -0,0 +1 @@ +Issue with `visibility` on GTK and `display` on all platform. diff --git a/core/src/toga/style/pack.py b/core/src/toga/style/pack.py index ac9787feeb..149685d888 100644 --- a/core/src/toga/style/pack.py +++ b/core/src/toga/style/pack.py @@ -86,7 +86,7 @@ def _debug(self, *args): # pragma: no cover @property def _hidden(self): "Does this style declaration define a object that should be hidden" - return self.visibility == HIDDEN + return self.visibility == HIDDEN or self.display == NONE def apply(self, prop, value): if self._applicator: @@ -104,8 +104,8 @@ def apply(self, prop, value): self._applicator.set_color(value) elif prop == "background_color": self._applicator.set_background_color(value) - elif prop == "visibility": - self._applicator.set_hidden(value == HIDDEN) + elif prop == "visibility" or prop == "display": + self._applicator.set_hidden(self._hidden) elif prop in ( "font_family", "font_size", @@ -275,6 +275,9 @@ def _layout_row_children( min_width = 0 remaining_width = available_width for child in node.children: + # Skip calculation if widget is not displayed + if child.style.display == NONE: + continue # self._debug(f"PASS 1 {child}") if child.style.width != NONE: # self._debug(f"- fixed width {child.style.width}") @@ -404,6 +407,9 @@ def _layout_row_children( # Pass 2: Lay out children with an intrinsic flexible width, # or no width specification at all. for child in node.children: + # Skip calculation if widget is not displayed + if child.style.display == NONE: + continue # self._debug(f"PASS 2 {child}") if child.style.width != NONE: # self._debug("- already laid out (explicit width)") @@ -489,6 +495,9 @@ def _layout_row_children( height = 0 min_height = 0 for child in node.children: + # Skip calculation if widget is not displayed + if child.style.display == NONE: + continue # self._debug(f"PASS 3: {child} AT HORIZONTAL {offset=}") if node.style.text_direction is RTL: # self._debug("- RTL") @@ -522,6 +531,9 @@ def _layout_row_children( # Pass 4: set vertical position of each child. for child in node.children: + # Skip calculation if widget is not displayed + if child.style.display == NONE: + continue # self._debug(f"PASS 4: {child}") extra = height - ( child.layout.content_height @@ -559,6 +571,9 @@ def _layout_column_children( min_height = 0 remaining_height = available_height for child in node.children: + # Skip calculation if widget is not displayed + if child.style.display == NONE: + continue # self._debug(f"PASS 1 {child}") if child.style.height != NONE: # self._debug(f"- fixed height {child.style.height}") @@ -689,6 +704,9 @@ def _layout_column_children( # Pass 2: Lay out children with an intrinsic flexible height, # or no height specification at all. for child in node.children: + # Skip calculation if widget is not displayed + if child.style.display == NONE: + continue # self._debug(f"PASS 2 {child}") if child.style.height != NONE: # self._debug("- already laid out (explicit height)") @@ -775,6 +793,9 @@ def _layout_column_children( width = 0 min_width = 0 for child in node.children: + # Skip calculation if widget is not displayed + if child.style.display == NONE: + continue # self._debug(f"PASS 3: {child} AT VERTICAL OFFSET {offset}") offset += child.style.padding_top child.layout.content_top = offset @@ -800,6 +821,9 @@ def _layout_column_children( # Pass 4: set horizontal position of each child. for child in node.children: + # Skip calculation if widget is not displayed + if child.style.display == NONE: + continue # self._debug(f"PASS 4: {child}") extra = width - ( child.layout.content_width diff --git a/gtk/src/toga_gtk/widgets/activityindicator.py b/gtk/src/toga_gtk/widgets/activityindicator.py index 54acfd3004..f8d2f7c342 100644 --- a/gtk/src/toga_gtk/widgets/activityindicator.py +++ b/gtk/src/toga_gtk/widgets/activityindicator.py @@ -16,9 +16,6 @@ def stop(self): self.native.stop() def rehint(self): - # print("REHINT", self, self.native.get_preferred_width(), self.native.get_preferred_height()) - width = self.native.get_preferred_width() - height = self.native.get_preferred_height() - + width, height = self._get_preferred_size(self.native) self.interface.intrinsic.width = width[0] self.interface.intrinsic.height = height[0] diff --git a/gtk/src/toga_gtk/widgets/base.py b/gtk/src/toga_gtk/widgets/base.py index d397d25eeb..4c3f139538 100644 --- a/gtk/src/toga_gtk/widgets/base.py +++ b/gtk/src/toga_gtk/widgets/base.py @@ -57,7 +57,6 @@ def container(self, container): # setting container, adding self to container.native self._container = container self._container.add(self.native) - self.native.show_all() for child in self.interface.children: child._impl.container = container @@ -84,6 +83,18 @@ def get_tab_index(self): def set_tab_index(self, tab_index): self.interface.factory.not_implemented("Widget.set_tab_index()") + def _get_preferred_size(self, native): + # Utility function to get the preferred size of widget regardless of it's visibility. + visible = native.get_visible() + if not visible: + native.set_visible(True) + # print("REHINT", self, native.get_preferred_width(), native.get_preferred_height()) + width = native.get_preferred_width() + height = native.get_preferred_height() + if not visible: + native.set_visible(visible) + return width, height + ###################################################################### # CSS tools ###################################################################### @@ -185,9 +196,6 @@ def refresh(self): def rehint(self): # Perform the actual GTK rehint. - # print("REHINT", self, self.native.get_preferred_width(), self.native.get_preferred_height()) - width = self.native.get_preferred_width() - height = self.native.get_preferred_height() - + width, height = self._get_preferred_size(self.native) self.interface.intrinsic.width = at_least(width[0]) self.interface.intrinsic.height = at_least(height[0]) diff --git a/gtk/src/toga_gtk/widgets/button.py b/gtk/src/toga_gtk/widgets/button.py index cf26e6a6f1..ec4c959a30 100644 --- a/gtk/src/toga_gtk/widgets/button.py +++ b/gtk/src/toga_gtk/widgets/button.py @@ -41,10 +41,7 @@ def set_background_color(self, color): super().set_background_color(color) def rehint(self): - # print("REHINT", self, self.native.get_preferred_width(), self.native.get_preferred_height()) - width = self.native.get_preferred_width() - height = self.native.get_preferred_height() - + width, height = self._get_preferred_size(self.native) self.interface.intrinsic.width = at_least(width[0]) self.interface.intrinsic.height = height[1] diff --git a/gtk/src/toga_gtk/widgets/canvas.py b/gtk/src/toga_gtk/widgets/canvas.py index 534c73e4f9..c2cf564041 100644 --- a/gtk/src/toga_gtk/widgets/canvas.py +++ b/gtk/src/toga_gtk/widgets/canvas.py @@ -299,7 +299,6 @@ def get_image_data(self): # Rehint def rehint(self): - # print("REHINT", self, self.native.get_preferred_width(), self.native.get_preferred_height()) # width = self.native.get_allocation().width # height = self.native.get_allocation().height width = self.interface._MIN_WIDTH diff --git a/gtk/src/toga_gtk/widgets/divider.py b/gtk/src/toga_gtk/widgets/divider.py index 09f6fa491d..c095e5ea73 100644 --- a/gtk/src/toga_gtk/widgets/divider.py +++ b/gtk/src/toga_gtk/widgets/divider.py @@ -9,9 +9,7 @@ def create(self): self.native = Gtk.Separator() def rehint(self): - width = self.native.get_preferred_width() - height = self.native.get_preferred_height() - + width, height = self._get_preferred_size(self.native) if self.get_direction() == self.interface.VERTICAL: self.interface.intrinsic.width = width[0] self.interface.intrinsic.height = at_least(height[1]) diff --git a/gtk/src/toga_gtk/widgets/label.py b/gtk/src/toga_gtk/widgets/label.py index f6ca0135ef..6dd472610f 100644 --- a/gtk/src/toga_gtk/widgets/label.py +++ b/gtk/src/toga_gtk/widgets/label.py @@ -24,12 +24,6 @@ def set_text(self, value): self.native.set_text(value) def rehint(self): - # print("REHINT", self, - # self.native.get_preferred_width(), self.native.get_preferred_height(), - # getattr(self, '_fixed_height', False), getattr(self, '_fixed_width', False) - # ) - width = self.native.get_preferred_width() - height = self.native.get_preferred_height() - + width, height = self._get_preferred_size(self.native) self.interface.intrinsic.width = at_least(width[0]) self.interface.intrinsic.height = height[1] diff --git a/gtk/src/toga_gtk/widgets/numberinput.py b/gtk/src/toga_gtk/widgets/numberinput.py index 176825bf3a..e3a190d569 100644 --- a/gtk/src/toga_gtk/widgets/numberinput.py +++ b/gtk/src/toga_gtk/widgets/numberinput.py @@ -64,9 +64,7 @@ def set_alignment(self, value): self.native.set_alignment(xalign) def rehint(self): - width = self.native.get_preferred_width() - height = self.native.get_preferred_height() - + width, height = self._get_preferred_size(self.native) self.interface.intrinsic.width = at_least( max(self.interface._MIN_WIDTH, width[1]) ) diff --git a/gtk/src/toga_gtk/widgets/progressbar.py b/gtk/src/toga_gtk/widgets/progressbar.py index f44ea3b026..19328835ed 100644 --- a/gtk/src/toga_gtk/widgets/progressbar.py +++ b/gtk/src/toga_gtk/widgets/progressbar.py @@ -91,9 +91,6 @@ def stop(self): self._stop_indeterminate() def rehint(self): - # print("REHINT", self, self.native.get_preferred_width(), self.native.get_preferred_height()) - width = self.native.get_preferred_width() - height = self.native.get_preferred_height() - + width, height = self._get_preferred_size(self.native) self.interface.intrinsic.width = at_least(width[0]) self.interface.intrinsic.height = height[0] diff --git a/gtk/src/toga_gtk/widgets/selection.py b/gtk/src/toga_gtk/widgets/selection.py index 7ff6ab2330..75cbaf309c 100644 --- a/gtk/src/toga_gtk/widgets/selection.py +++ b/gtk/src/toga_gtk/widgets/selection.py @@ -89,8 +89,7 @@ def get_selected_index(self): return index def rehint(self): - width = self.native.get_preferred_width() - height = self.native.get_preferred_height() + width, height = self._get_preferred_size(self.native) # FIXME: 2023-05-31 This will always provide a size that is big enough, # but sometimes it will be *too* big. For example, if you set the font size diff --git a/gtk/src/toga_gtk/widgets/slider.py b/gtk/src/toga_gtk/widgets/slider.py index 301cbfa356..02c645c9bd 100644 --- a/gtk/src/toga_gtk/widgets/slider.py +++ b/gtk/src/toga_gtk/widgets/slider.py @@ -82,8 +82,7 @@ def get_tick_count(self): return self.tick_count def rehint(self): - # print("REHINT", self, self.native.get_preferred_width(), self.native.get_preferred_height()) - height = self.native.get_preferred_height() + width, height = self._get_preferred_size(self.native) # Set intrinsic width to at least the minimum width self.interface.intrinsic.width = at_least(self.interface._MIN_WIDTH) diff --git a/gtk/src/toga_gtk/widgets/switch.py b/gtk/src/toga_gtk/widgets/switch.py index 8c86d2bc1f..31450bac2d 100644 --- a/gtk/src/toga_gtk/widgets/switch.py +++ b/gtk/src/toga_gtk/widgets/switch.py @@ -52,12 +52,8 @@ def set_font(self, font): self.apply_css("font", get_font_css(font), native=self.native_label) def rehint(self): - # print("REHINT", self, self.native.get_preferred_width(), self.native.get_preferred_height()) - label_width = self.native_label.get_preferred_width() - label_height = self.native_label.get_preferred_height() - - switch_width = self.native_switch.get_preferred_width() - switch_height = self.native_switch.get_preferred_height() + label_width, label_height = self._get_preferred_size(native=self.native_label) + switch_width, switch_height = self._get_preferred_size(native=self.native_label) # Set intrinsic width to at least the minimum width self.interface.intrinsic.width = at_least( diff --git a/gtk/src/toga_gtk/widgets/textinput.py b/gtk/src/toga_gtk/widgets/textinput.py index ecce9555d5..d149b30256 100644 --- a/gtk/src/toga_gtk/widgets/textinput.py +++ b/gtk/src/toga_gtk/widgets/textinput.py @@ -55,13 +55,7 @@ def set_value(self, value): self.native.set_text(value) def rehint(self): - # print("REHINT", self, - # self._impl.get_preferred_width(), self._impl.get_preferred_height(), - # getattr(self, '_fixed_height', False), getattr(self, '_fixed_width', False) - # ) - width = self.native.get_preferred_width() - height = self.native.get_preferred_height() - + width, height = self._get_preferred_size(self.native) self.interface.intrinsic.width = at_least( max(self.interface._MIN_WIDTH, width[1]) ) diff --git a/gtk/src/toga_gtk/window.py b/gtk/src/toga_gtk/window.py index 0273f62424..82cab211aa 100644 --- a/gtk/src/toga_gtk/window.py +++ b/gtk/src/toga_gtk/window.py @@ -144,7 +144,10 @@ def set_app(self, app): app.native.add_window(self.native) def show(self): - self.native.show_all() + # Avoid calling show_all() as it change the visibility of all children. + self.native.show() + self.layout.show() + self.container.show() ###################################################################### # Window content and resources From 94d599996bd5985a73e9d1205afc072c95494158 Mon Sep 17 00:00:00 2001 From: Patrik Dufresne Date: Fri, 1 Mar 2024 16:30:21 -0500 Subject: [PATCH 2/2] WIP: Add Label wrap for GTK and Winforms --- core/src/toga/style/pack.py | 5 +++ core/src/toga/widgets/label.py | 11 ++++++ gtk/src/toga_gtk/widgets/base.py | 5 +-- gtk/src/toga_gtk/widgets/label.py | 18 +++++++++ temp.py | 44 +++++++++++++++++++++ winforms/src/toga_winforms/widgets/label.py | 27 +++++++++++-- 6 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 temp.py diff --git a/core/src/toga/style/pack.py b/core/src/toga/style/pack.py index 149685d888..4eb60f1018 100644 --- a/core/src/toga/style/pack.py +++ b/core/src/toga/style/pack.py @@ -236,6 +236,11 @@ def _layout_node( else: width = available_width height = available_height + if hasattr(node._impl, 'compute_size'): + # To support text wraping, when fixing the width, we need to recompute the height. + width, height = node._impl.compute_size(width, height) + min_height = height + # self._debug(f"NO CHILDREN {min_width=} {width=} {min_height=} {height=}") # If an explicit width/height was given, that specification diff --git a/core/src/toga/widgets/label.py b/core/src/toga/widgets/label.py index 88a5fd1fad..89e26e8d8f 100644 --- a/core/src/toga/widgets/label.py +++ b/core/src/toga/widgets/label.py @@ -9,6 +9,7 @@ def __init__( text: str, id=None, style=None, + wrap=False, ): """Create a new text label. @@ -23,6 +24,7 @@ def __init__( self._impl = self.factory.Label(interface=self) self.text = text + self.wrap = wrap def focus(self): "No-op; Label cannot accept input focus" @@ -47,3 +49,12 @@ def text(self, value): self._impl.set_text(text) self.refresh() + + @property + def wrap(self) -> str: + return self._impl.get_wrap() + + @text.setter + def wrap(self, value): + self._impl.set_wrap(bool(value)) + self.refresh() \ No newline at end of file diff --git a/gtk/src/toga_gtk/widgets/base.py b/gtk/src/toga_gtk/widgets/base.py index 4c3f139538..f976eeb68a 100644 --- a/gtk/src/toga_gtk/widgets/base.py +++ b/gtk/src/toga_gtk/widgets/base.py @@ -89,11 +89,10 @@ def _get_preferred_size(self, native): if not visible: native.set_visible(True) # print("REHINT", self, native.get_preferred_width(), native.get_preferred_height()) - width = native.get_preferred_width() - height = native.get_preferred_height() + min_size, nat_size = native.get_preferred_size() if not visible: native.set_visible(visible) - return width, height + return (min_size.width, nat_size.width),(min_size.height, nat_size.height) ###################################################################### # CSS tools diff --git a/gtk/src/toga_gtk/widgets/label.py b/gtk/src/toga_gtk/widgets/label.py index 6dd472610f..5cfca7e05c 100644 --- a/gtk/src/toga_gtk/widgets/label.py +++ b/gtk/src/toga_gtk/widgets/label.py @@ -22,8 +22,26 @@ def get_text(self): def set_text(self, value): self.native.set_text(value) + + def get_wrap(self): + self.native.get_line_wrap() + + def set_wrap(self, wrap): + self.native.set_line_wrap(wrap) def rehint(self): width, height = self._get_preferred_size(self.native) self.interface.intrinsic.width = at_least(width[0]) self.interface.intrinsic.height = height[1] + + def compute_size(self, width, height): + + if width and self.native.get_line_wrap(): + # If wrapping is enabled, calculate prefered height with fixed width + req_width, req_height = self.native.get_size_request() + self.native.set_size_request(width, -1) + _unused, height = self._get_preferred_size(self.native) + self.native.set_size_request(req_width, req_height) + return width, height[0] + + return width, height diff --git a/temp.py b/temp.py new file mode 100644 index 0000000000..842812a166 --- /dev/null +++ b/temp.py @@ -0,0 +1,44 @@ +import toga +from toga.constants import ROW +from toga.style.pack import Pack +import toga.colors +import sys + +IS_LINUX = sys.platform in ["linux", "linux2"] + +class TestApp(toga.App): + def startup(self): + # Re-enable pydev debugging in this thread. + try: + import pydevd; + pydevd.settrace() + except ImportError: + pass + self.main_window = toga.MainWindow(size=(300, 150)) + + style = Pack(padding_top=24) + substyle = Pack(padding_right=12, padding_left=12, flex=1) + + # Add the content on the main window + self.main_window.content = toga.Box( + children=[ + toga.Button(text="Toggle"), + toga.Label(text="This is a very long line of text to test the wrapping feature in GTK.", style=Pack(flex=1,background_color=toga.colors.BLUE), wrap=1), + toga.TextInput(placeholder="Footer"), + ], + style=Pack(direction=ROW, padding=24), + ) + + # Show the main window + self.main_window.show() + +def main(): + # Application class + # App name and namespace + app = TestApp("Test", "org.beeware.toga.examples.test") + return app + + +if __name__ == "__main__": + app = main() + app.main_loop() diff --git a/winforms/src/toga_winforms/widgets/label.py b/winforms/src/toga_winforms/widgets/label.py index c50f59b5ae..93f098ae3c 100644 --- a/winforms/src/toga_winforms/widgets/label.py +++ b/winforms/src/toga_winforms/widgets/label.py @@ -1,7 +1,9 @@ from decimal import ROUND_UP +import System.Drawing import System.Windows.Forms as WinForms from travertino.size import at_least +from ..libs.wrapper import WeakrefCallable from toga_winforms.libs.fonts import TextAlignment @@ -12,6 +14,7 @@ class Label(Widget): def create(self): self.native = WinForms.Label() self.native.AutoSizeMode = WinForms.AutoSizeMode.GrowAndShrink + self._wrap = False def set_alignment(self, value): self.native.TextAlign = TextAlignment(value) @@ -22,10 +25,28 @@ def get_text(self): def set_text(self, value): self.native.Text = value + def get_wrap(self): + return self._wrap + + def set_wrap(self, wrap): + self._wrap = wrap + def rehint(self): - self.interface.intrinsic.width = self.scale_out( - at_least(self.native.PreferredSize.Width), ROUND_UP - ) + if self._wrap: + # When text wrapping is enabled we need to return the minimum width. + proposed_size = System.Drawing.Size(1, 0) + width = WinForms.TextRenderer.MeasureText( self.native.Text, self.native.Font, proposed_size, WinForms.TextFormatFlags.WordBreak).Width + else: + width = self.native.PreferredSize.Width + self.interface.intrinsic.width = self.scale_out(at_least(width), ROUND_UP) self.interface.intrinsic.height = self.scale_out( self.native.PreferredSize.Height, ROUND_UP ) + + def compute_size(self, width, height): + if self._wrap: + # When text wrapping is enabled, we need to do additional calculation to determine the proper height. + width = self.scale_out(width,ROUND_UP) + proposed_size = System.Drawing.Size(width, 0) + height = WinForms.TextRenderer.MeasureText( self.native.Text, self.native.Font, proposed_size, WinForms.TextFormatFlags.WordBreak).Height + return width, height \ No newline at end of file