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
Add property to map or unmap a Widget #8645
Comments
As a workaround you could add a method to Widget: from kivy.app import App
from kivy.lang import Builder
from kivy.uix.widget import Widget
kv = """
BoxLayout:
orientation: 'vertical'
AnchorLayout:
Button:
id: button
text: 'Make me disappear'
size_hint: None, None
size: dp(150), dp(48)
on_release: print('Hello')
ToggleButton:
size_hint_y: None
height: dp(48)
text: 'display on/off'
on_state:
if self.state == 'down': button.display('hide')
if self.state == 'normal': button.display('show')
"""
def display(self, mode):
if mode == 'hide':
self._og_size_hint = self.size_hint.copy()
self._og_size = self.size.copy()
self.size_hint = None, None
self.size = 0, 0
self.disabled = True
self.opacity = 0
elif mode == 'show':
self.size_hint = self._og_size_hint
self.size = self._og_size
self.disabled = False
self.opacity = 1
else:
raise ValueError
Widget.display = display
class AddMethodToWidgetApp(App):
def build(self):
return Builder.load_string(kv)
AddMethodToWidgetApp().run() Another option to consider depending on your use case, is to use a ScreenManager and Screens. |
I wanted to make display a kivy property, so made the changed below. This is a very reasonable workaround. from kivy.app import App
from kivy.lang import Builder
from kivy.uix.widget import Widget
kv = """
<Widget>:
display: True # create a kivy property
on_display: self.display_action(*args)
BoxLayout:
orientation: 'vertical'
AnchorLayout:
Button:
id: button
text: 'Make me disappear'
size_hint: None, None
size: dp(150), dp(48)
on_release: print('Hello')
ToggleButton:
size_hint_y: None
height: dp(48)
text: 'display on/off'
on_state:
button.display = self.state == 'normal'
"""
def _display_action(self, obj, value):
if not value:
self._og_size_hint = self.size_hint
self._og_size = self.size.copy()
self.size_hint = None, None
self.size = 0, 0
self.disabled = True
self.opacity = 0
else:
self.size_hint = self._og_size_hint
self.size = self._og_size
self.disabled = False
self.opacity = 1
Widget.display_action = _display_action
class AddMethodToWidgetApp(App):
def build(self):
return Builder.load_string(kv)
AddMethodToWidgetApp().run() |
@ElliotGarbus After a couple trial and error, I came up with this solution where I create a chain list of widget when required. When a widget is set with display=False, I remove it from the parent (this effectively remove all the drawing related to it). When display=true, I'm adding it back.
|
Glad to see you have something that works for your use case. I don't really understand your use case, but it seems like you could use a ScreenManager to make the widgets disappear. |
Although your solution is good for some cases, I believe the from kivy.app import App
from kivy.factory import Factory as F
from kivy.lang import Builder
from kivy.uix.widget import Widget
kv = Builder.load_string(
"""
<Widget>:
display: True # create a kivy property
on_display: self.display_action(*args)
<MainScreen@Screen>:
condition: True
GridLayout:
cols: 3 if root.condition else 2
spacing: dp(100)
Button:
text: '1'
Button:
text: '2'
Button:
text: '3'
display: False
"""
)
def _display_action(self, obj, value):
if not value:
self._og_size_hint = self.size_hint
self._og_size = self.size.copy()
self.size_hint = None, None
self.size = 0, 0
self.disabled = True
self.opacity = 0
else:
self.size_hint = self._og_size_hint
self.size = self._og_size
self.disabled = False
self.opacity = 1
Widget.display_action = _display_action
class MainApp(App):
def build(self):
return F.MainScreen()
def some_function(self):
print("Hello World")
MainApp().run() Also, depending on the specifics of your Button rendering, the from kivy.app import App
from kivy.factory import Factory as F
from kivy.lang import Builder
from kivy.uix.widget import Widget
from kivy_widgets.buttons import CButton
kv = Builder.load_string(
"""
<Widget>:
display: True # create a kivy property
on_display: self.display_action(*args)
<MainScreen@Screen>:
condition: True
canvas.before:
Color:
rgba: 1, 0, 0, .1
Rectangle:
size: self.size
pos: self.pos
GridLayout:
cols: 3 if root.condition else 2
Button:
text: '1'
CButton:
text: '2'
display: False
Button:
text: '3'
"""
)
def _display_action(self, obj, value):
if not value:
self._og_size_hint = self.size_hint
self._og_size = self.size.copy()
self.size_hint = None, None
self.size = 0, 0
self.disabled = True
self.opacity = 0
else:
self.size_hint = self._og_size_hint
self.size = self._og_size
self.disabled = False
self.opacity = 1
Widget.display_action = _display_action
class MainApp(App):
def build(self):
return F.MainScreen()
def some_function(self):
print("Hello World")
MainApp().run() |
Could you expand on that or provide an example? |
@FilipeMarch Yes, I see your point, if you use a ScreenManager, the same problem exists. The space is still there. Here is an example using the ScreenManager to hide the widget. from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, NoTransition
from kivy.properties import BooleanProperty
kv = """
<DisplayButton>:
Screen:
name: 'on_screen'
Button:
text: 'Make me disappear'
on_release: print('Hello')
Screen:
name: 'off_screen'
BoxLayout:
orientation: 'vertical'
AnchorLayout:
DisplayButton:
id: db
size_hint: None, None
size: dp(150), dp(48)
ToggleButton:
size_hint_y: None
height: dp(48)
text: 'display on/off'
on_state:
db.display = self.state == 'normal'
"""
class DisplayButton(ScreenManager):
display = BooleanProperty(True)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.transition = NoTransition()
def on_display(self, obj, value):
self.current = 'on_screen' if value else 'off_screen'
class AddMethodToWidgetApp(App):
def build(self):
return Builder.load_string(kv)
AddMethodToWidgetApp().run() I suppose the screens could be sized rather than the ScreenManager... |
Thanks for the example, I understand it now. <DisplayButton>:
Screen:
name: 'on_screen'
Button:
text: 'Make me disappear'
on_release: print('Hello')
Screen:
name: 'off_screen'
class DisplayButton(ScreenManager):
display = BooleanProperty(True)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.transition = NoTransition()
def on_display(self, obj, value):
self.current = "on_screen" if value else "off_screen" How many lines does this has? It could be even bigger depending what the user actually wants. Button:
render: False This would actually remove the widget from the widget tree. |
How about as a mixin... from kivy.app import App
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.properties import BooleanProperty
kv = """
BoxLayout:
orientation: 'vertical'
AnchorLayout:
DisplayButton:
id: button
text: 'Make me disappear'
size_hint: None, None
size: dp(150), dp(48)
on_release: print('Hello')
ToggleButton:
size_hint_y: None
height: dp(48)
text: 'display on/off'
on_state:
button.display = self.state == 'normal'
"""
class DisplayBehavior:
display = BooleanProperty(True)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._parent = None
self._widget = None
def on_display(self, obj, value):
if not value:
self._parent = self.parent
self._widget = self
self._parent.remove_widget(self)
else:
self._parent.add_widget(self._widget)
class DisplayButton(DisplayBehavior, Button):
pass
class AddMethodToWidgetApp(App):
def build(self):
return Builder.load_string(kv)
AddMethodToWidgetApp().run() |
Very good idea. What about saving the initial position? Because the tree order can change. from kivy.app import App
from kivy.factory import Factory as F
from kivy.lang import Builder
from kivy.properties import BooleanProperty
from kivy.uix.button import Button
class DisplayBehavior:
display = BooleanProperty(True)
initial_position = F.NumericProperty()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._parent = None
self._widget = None
def on_display(self, obj, value):
if not value:
self._parent = self.parent
self._widget = self
self.initial_position = self._parent.children.index(self)
self._parent.remove_widget(self)
else:
self._parent.add_widget(self._widget, index=self.initial_position)
class DisplayAnchorLayout(DisplayBehavior, F.AnchorLayout):
pass
kv = """
BoxLayout:
orientation: 'vertical'
on_children: print('children changed')
DisplayAnchorLayout:
id: button
DisplayButton:
text: 'Make me disappear'
size_hint: None, None
size: dp(150), dp(48)
on_release: print('Hello')
ToggleButton:
size_hint_y: None
height: dp(48)
text: 'display on/off'
on_state:
button.display = self.state == 'normal'
"""
class DisplayButton(DisplayBehavior, Button):
pass
class AddMethodToWidgetApp(App):
def build(self):
return Builder.load_string(kv)
AddMethodToWidgetApp().run() |
@FilipeMarch Yes, we need to keep the initial position. That the most callengin part. If you look at my implementation that more or less the general idea. It to keep reference to previous widget in the tree to know where to insert it. If you have multiple children getting hidden, you cannot use index to restore the widget at the right location. |
Well I think one solution would be to save the index of each widget in the tree, then before adding a widget back to its parent you check all the indexes and make sure you insert in such a way all indexes are still in increasing order Here I asked ChatGPT for such an half baked implementation idea: https://chat.openai.com/share/b97d8776-4620-485d-8442-c7d02a82a365 For example initial indexes: [1,2,3,4,5] Now suppose you set widget_4.display = True Now you just need a function that will receives [1,3,5] as arg and outputs 2, which is the index the widget_4 must be added I probably won't enter on computer today so I may implement it until Monday. You can wait or follow my reasoning, I hope it helps |
Preserving the order adds a number of complications. Assume we have a layout that includes some Widgets with DisplayBeahvior and some without. The widget lacks a mechanism to track additions/removals from the enclosing layout. The add_widget, remove_widget methods of the enclosing layout would need to be overloaded to capture the changes. A shadow children list would need to track the order of the widgets. The change in display state could remove and re-add all of the child widgets that have the display attribute equal True as a way to preserve order. This suggests a more robust solution would need to be implemented at the layout level. DisplayParentBehavior or DisplayLayoutBehavior for an enclosing Layout that includes widgets with the display attribute. This behavior could add the display attribute to all the child widgets. Things get more complicated with nesting, and the addition of Layouts under Layouts... Restricting the requirements can simplify the implementation. |
Here is a prototype that adds a behavior to the layout and the widgets. I added a name to the widgets as a way to access the widget to show/hide. from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.properties import BooleanProperty, StringProperty
"""
Prototype behaviors to remove/restore widgets in a layout
TODO: raise exception if a Layout or a widget without DisplayWidgetBehavior is added
"""
class DisplayWidgetBehavior:
display = BooleanProperty(True)
name = StringProperty() # use name to access widget
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._parent = None
def on_parent(self, obj, value):
if value:
self._parent = value
def on_display(self, obj, value):
if value:
self._parent.restore_widget()
else:
self._parent.remove_widget(self)
class DisplayLayoutBehavior:
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._shadow = [] # used to hold the list of all the widgets in the layout hidden and visible
def add_widget(self, widget, index=0):
super().add_widget(widget, index)
if widget not in self._shadow:
self._shadow = self.children.copy()
def display_toggle(self, name):
for w in self._shadow:
if w.name == name:
w.display = not w.display
break
def restore_widget(self):
self.clear_widgets()
for w in self._shadow[::-1]:
if w.display:
super().add_widget(w)
class DisplayBoxLayout(DisplayLayoutBehavior, BoxLayout):
pass
class DisplayButton(DisplayWidgetBehavior, Button):
pass
kv = """
BoxLayout:
orientation: 'vertical'
Label:
size_hint_y: None
height: dp(30)
text: 'Test Display Layout'
AnchorLayout:
DisplayBoxLayout:
id: display_layout
size_hint: None, None
size: dp(500), dp(48)
DisplayButton:
text: 'one'
name: self.text
DisplayButton:
text: 'two'
name: self.text
DisplayButton:
text: 'three'
name: self.text
BoxLayout:
size_hint_y: None
height: dp(48)
Button:
text: 'one'
on_release: display_layout.display_toggle('one')
Button:
text: 'two'
on_release: display_layout.display_toggle('two')
Button:
text: 'three'
on_release: display_layout.display_toggle('three')
"""
class DisplayTestApp(App):
def build(self):
return Builder.load_string(kv)
DisplayTestApp().run() |
@ElliotGarbus That's awesome, bro. Your solution is very neat, I haven't found any problems until now. I will keep using it for some time. I'll let you know if I find any issues. |
@ElliotGarbus Thanks for your POC. Your recommendation provide a way to solve part of the issue. If this implementation is adopted directly in Kivy, it would be nice that a widget with |
There's a problem with not changing the widget tree that I pointed at #8645 (comment) |
@FilipeMarch I'm not a Kivy expert, but I would think it might be possible to change the way Layout are working to make sure of an Alias Property(e.g.: _display_children) that would be a reflection of children property that only include widget with display. Then GridLayout and other widget could make use of only that property to compute the posotion and spacing, etc. |
Here is an updated prototype. This makes a few improvements and segregates the test code such that the file can be imported directly: from kivy.uix.layout import Layout
from kivy.properties import BooleanProperty
"""
Prototype behaviors to remove/restore widgets in a layout.
A property "display" is added to a widget. If display is True the widget is visible. If display is False
the widget is removed from the widget tree, and is not visible. The goal is the be able to remove widgets
from the layout and later restore them while maintaining the relative position of the widgets.
The code defines 2 behaviors:
DisplayWidgetBehavior: adds a property, display, to the widget. display is a BooleanProperty.
When display is True the widget is visible, with display is False, the widget is removed
from the widget tree.
DisplayLayoutBehavior: must be added to the layout that will enclose the display widgets. It preserves the order
of the widgets.
For simplicity, the DisplayLayoutBehavior does not support Nested Layouts
"""
class DisplayWidgetBehavior:
display = BooleanProperty(True)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._parent = None
def on_parent(self, obj, value):
if value:
self._parent = value
def on_display(self, obj, value):
if value:
self._parent.restore_widget()
else:
self._parent.hide_widget(self)
class DisplayLayoutBehavior:
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._shadow = [] # holds a copy of the widgets
self.clearing = False
def add_widget(self, widget, index=0):
if not isinstance(widget, DisplayWidgetBehavior):
raise TypeError('Child Widgets must have DisplayWidgetBehavior')
if isinstance(widget, Layout):
raise TypeError('Nested Layouts not supported')
super().add_widget(widget, index)
if widget not in self._shadow:
self._shadow.insert(index, widget)
def remove_widget(self, widget):
super().remove_widget(widget)
if not self.clearing:
self._shadow.remove(widget)
def hide_widget(self, widget):
super().remove_widget(widget)
def restore_widget(self):
self._remove_all_widgets()
for w in self._shadow[::-1]:
if w.display:
super().add_widget(w)
def _remove_all_widgets(self):
# this clears the widget list, but preserves the shadow list
self.clearing = True
super().clear_widgets()
self.clearing = False
if __name__ == '__main__':
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from textwrap import dedent
class DisplayBoxLayout(DisplayLayoutBehavior, BoxLayout):
pass
class DisplayButton(DisplayWidgetBehavior, Button):
pass
kv = dedent("""
BoxLayout:
orientation: 'vertical'
Label:
size_hint_y: None
height: dp(30)
text: 'Test Display Layout'
AnchorLayout:
DisplayBoxLayout:
id: display_layout
size_hint: None, None
size: dp(500), dp(48)
DisplayButton:
id: btn_1
text: 'one'
name: self.text
DisplayButton:
id: btn_2
text: 'two'
name: self.text
DisplayButton:
id: btn_3
text: 'three'
name: self.text
BoxLayout:
size_hint_y: None
height: dp(48)
Button:
text: 'one'
on_release: btn_1.display = not btn_1.display
Button:
text: 'two'
on_release: btn_2.display = not btn_2.display
Button:
text: 'three'
on_release: btn_3.display = not btn_3.display
ToggleButton:
text: 'Add/remove button 4'
on_state: app.add_remove(self.state)
ToggleButton:
id: show_hide
disabled: True
text: 'hide/show button 4'
on_state: app.show_hide(self.state)
""")
class DisplayTestApp(App):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.db_4 = DisplayButton(text='four')
def build(self):
return Builder.load_string(kv)
def add_remove(self, state):
if state == 'down':
self.root.ids.display_layout.add_widget(self.db_4)
self.root.ids.show_hide.disabled = False
else:
self.root.ids.display_layout.remove_widget(self.db_4)
self.db_4.display = True
self.root.ids.show_hide.disabled = True
self.root.ids.show_hide.state = 'normal'
def show_hide(self, state):
self.db_4.display = {'normal': True, 'down': False }[state]
DisplayTestApp().run() Edit: added some minimal documentation |
Is your feature request related to a problem? Please describe.
As I need to dynamically show/hide widget based on properties. Current implementation required to add or remove the widget which required alot of extra code and get error prone when you have many widget getting show or hide based on different condition.
Describe the solution you'd like
I would like to have a
display
property on all widget to control if the Widget should get display or not by the layout.This is not the same as
opacity
which control if the widget is visible.This
display
property is similar to CSS display property. When display is none, the HTML element is completely hidden, doesn't occupy space and user cannot interact with it.Describe alternatives you've considered
I tried to make use of opacity and enabled disable, but widget still occupy spaces in the layout. Also updating 3 properties to hide a widget is getting very error prone. e.g.: opacity:0, height: 0, disable: True vs display: False
I Also tried to implement my own version of display property, but I'm quickly running into trouble. Keeping the ordering of the widget is difficult with add_widget and remove_widget if you have multiple widget that get hidden and shown based on different condition.
Additional context
N/A
The text was updated successfully, but these errors were encountered: