Kivy Label add text without overlapping - python

I am making a simple chat room application, and it is working perfectly other than the fact that when a user enters a long message, it overlaps the other messages in the ScrollView like this:
This is the code for the label:
message_lab = Label(text=text, markup=True, size_hint_y=None)
message_lab.width = self.width
message_lab.text_size = message_lab.width, None
self.chat.add_widget(message_lab)
self.scroller.scroll_to(message_lab)
Note(s):
The value of self.width is 800
self.chat is a BoxLayout which is the layout inside the ScrollView
self.scroller is the ScrollView
Any help I receive is greatly appreciated!

Use a kv rule and Label subclass, something like:
<YourLabel>:
height: self.texture_size[1]
text_size: self.width, None

Related

Kivy Layout Background Not Painted on ScrollView

Please consider the following code:
Builder.load_string(
"""
<TestView>:
canvas.before:
Color:
rgba: 0, 1, 0, 1
Rectangle:
pos: self.pos
size: self.size
""")
class TestView(GridLayout):
def __init__(self, **kwargs):
super(TestView, self).__init__(**kwargs)
self.cols = 1
self.spacing = 20
self.add_widget(Label(text="I'm on the TestView", height=80 ))
class TestScreen(Screen):
def __init__(self, **kwargs):
super(TestScreen, self).__init__(**kwargs)
self.content_grid_layout = GridLayout(cols=1, spacing=20, size_hint_y=None)
self.content_grid_layout.bind(minimum_height=self.content_grid_layout.setter('height'))
self.content_grid_layout.add_widget(Label(text="A"))
self.content_grid_layout.add_widget(Label(text="B"))
self.content_grid_layout.add_widget(TestView())
self.scroll_view = ScrollView(size_hint=(1, 1), size=(self.width, self.height))
self.scroll_view.add_widget(self.content_grid_layout)
self.add_widget(self.scroll_view)
sm = ScreenManager()
sm.add_widget(TestScreen(name="test_screen"))
class TestApp(App):
def build(self):
return sm
if __name__ == '__main__':
TestApp().run()
(I've left out the imports to save space). So my specific problem here is that I expect the background of TestView to be painted green, but it isn't here. It would be if you just add TestView straight onto the Screen's layout, i.e. change the init of TestScreen to just do:
self.content_grid_layout = GridLayout(cols=1, spacing=20, size_hint_y=None)
self.content_grid_layout.add_widget(TestView())
self.add_widget(self.content_grid_layout)
However, I need my content to be on a ScrollView here. So I also note that if I just comment out this line:
self.content_grid_layout.bind(minimum_height=self.content_grid_layout.setter('height'))
from the original code, everything appears to work as expected and the background of the TestView is painted green. However, if I do that in turns out the ScrollView isn't doing what I expect (it doesn't scroll).
If I give the TestView an explicit height, then everything works as expected, e.g. add:
self.size_hint_y = None
self.height = 40
to the TestView init. So I guess the problem is something not really knowing what height to give the TestView, but then weirdly being able to put the Label in approximately the right place, but not paint the background, or paint it off the screen or something. Anyway, hard-coding the TestView doesn't work for me because IRL it will have some dynamic content that will have varying heights.
I feel like in a sane world the height of a layout would be the height of its contents, unless otherwise specified, but maybe that's just me. I think if I can size the TestView to its contents then I'd get the behaviour I'm expecting?
Edit:
OK, I think I can get it to do what I want by following the strategy described here: https://groups.google.com/d/msg/kivy-users/xRl2l8-1qLs/zzpk-QG4C0MJ
(in particular the CustomGridLayout described there which is like my TestView I guess).
So the basic idea it seems is that we set the custom GridLayout (TestView) to zero height initially, then manually update its height for each child we add, so something like:
class TestView(GridLayout):
def __init__(self, **kwargs):
super(TestView, self).__init__(**kwargs)
self.cols = 1
self.spacing = 20, 20
self.size_hint_y = None
self.height = 0
self.height = self.height + 30 + self.spacing[1]
self.add_widget(Label(text="I'm on the TestView 1", size_hint_y=None, height=30))
self.height = self.height + 30 + self.spacing[1]
self.add_widget(Label(text="I'm on the TestView 2", size_hint_y=None, height=30))
self.height = self.height + 30 + self.spacing[1]
self.add_widget(Label(text="I'm on the TestView 3", size_hint_y=None, height=30))
I'm not going to lie, I think this is pretty ugly. It really seems that the layout should be able to work out its new height when I add a widget to it without having to spoon feed it like this. Anyway, it seems to work. I'm going to leave the question open in case someone has a not-horrible way of doing this.
By commenting out the line you mentioned (self.content_grid_layout.bind(minimum_height=self.content_grid_layout.setter('height'))) and adding height: self.minimum_height to the Builder string I was able to get your TestView to be green. The TestScreen is also scrollable.
Consider changing the TestScreen initializer to this:
def __init__(self, **kwargs):
super(TestScreen, self).__init__(**kwargs)
self.content_grid_layout = GridLayout(cols=1, spacing=20, size_hint_y=None)
# self.content_grid_layout.bind(minimum_height=self.content_grid_layout.setter('height'))
self.content_grid_layout.add_widget(Label(text="A"))
self.content_grid_layout.add_widget(Label(text="B"))
self.content_grid_layout.add_widget(TestView())
self.scroll_view = ScrollView(size_hint=(1, 1), size=(self.width, self.height))
self.scroll_view.add_widget(self.content_grid_layout)
self.add_widget(self.scroll_view)
And the Builder string to this:
Builder.load_string(
"""
<TestView>:
height: self.minimum_height
canvas.before:
Color:
rgba: 0, 1, 0, 1
Rectangle:
pos: self.pos
size: self.size
""")
OK, I think I've cracked it - Edvardas' notion of binding the TestView's minimum height to its height is correct (and I guess maybe this should have been obvious to me since it's also what we do for the 'content_grid_layout'). However, it wasn't working because we also need 'size_hint_y: None'.
I don't think we should take out the height / minimum height binding from 'content_grid_layout' because that stops scrolling working.
Doing this it works as expected and we don't have to manually set the TestView's height. Here's a full working example to save any confusion - I've changed the TestView from a GridLayout to a BoxLayout but it works as a GridLayout too:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.scrollview import ScrollView
Builder.load_string(
"""
<TestView>:
size_hint_y: None # <----------------- MAKE SURE YOU DO THIS!
height: self.minimum_height # <----------------- AND THIS!
canvas.before:
Color:
rgba: 0, 0.8, 0.06, 0.5
RoundedRectangle:
pos: self.pos
size: self.size
radius: [16,16,16,16]
""")
class TestView(BoxLayout):
def __init__(self, **kwargs):
super(TestView, self).__init__(**kwargs)
self.orientation = 'vertical'
self.padding = [20, 20, 20, 20]
self.spacing = 20
self.add_widget(Label(text="I'm on the TestView 1"))
self.add_widget(Label(text="I'm on the TestView 2"))
self.add_widget(Label(text="I'm on the TestView 3"))
class TestScreen(Screen):
def __init__(self, **kwargs):
super(TestScreen, self).__init__(**kwargs)
self.content_grid_layout = GridLayout(cols=1, spacing=20, size_hint_y=None)
self.content_grid_layout.bind(minimum_height=self.content_grid_layout.setter('height'))
# Give us something to scroll:
for i in range(20):
btn = Button(text=str(i), size_hint_y=None, height=40)
self.content_grid_layout.add_widget(btn)
if i == 5:
self.content_grid_layout.add_widget(TestView())
self.scroll_view = ScrollView(size_hint=(1, 1), size=(self.width, self.height))
self.scroll_view.add_widget(self.content_grid_layout)
self.add_widget(self.scroll_view)
sm = ScreenManager()
sm.add_widget(TestScreen(name="test_screen"))
class TestApp(App):
def build(self):
return sm
if __name__ == '__main__':
TestApp().run()

In kivy, How can I control the height of the VKeyboard used by TextInput

I have a Screen where I want to ask a question, so in the kv language, I have
Screen:
name: 'keyb'
BoxLayout:
orientation: 'vertical'
Label:
id: rnl
size_hint_y: .1
text: ''
TextInput:
id: tinput
hint_text: '.mid'
size_hint: (.8, .1)
pos_hint: {'x': .1}
multiline: False
on_focus: root.focusCallback(args[1])
Widget:
size_hint_y: .7
When I want to ask the question, I select the screen and set the focus to the TextInput
self.tinput.text = ""
self.screens.current = 'keyb'
Clock.schedule_once(self.focusKbd, .1)
which then cascades through this code:
def focusKbd(self, x):
self.tinput.focus = True
def focusCallback(self, f):
if not f:
# Deal with input now
self.screens.current = 'seq'
Somewhere in this code, I would like to
Select which VKeyboard layout the TextInput is going to pop up (and it will be different in other parts of my code)
Adjust the height of the VKeyboard.
I don't understand where the VKeyboard comes from in Text Input; is there some way I can get a reference to it from the TextInput?
This is a restatement of https://stackoverflow.com/questions/36414654/how-can-i-change-the-size-of-kivys-vkeyboard There is a hint there, but not enough to get me going!
*********************Edit: **************************
Trying to understand the answer from Tshirtman, I put this at the top of my main widget:
Window.set_vkeyboard_class(get_vkeyboard())
and then added
def get_vkeyboard():
print '>>>>>>>>>>>>>>>>>>>>>>>>>>in get_vkeyboard'
return VKeyboard
I found that get_vkeyboard() was called immediately, long before I needed a keyboard. So it seems that I'm not going to be able to control anything about the appearance of the VKeyboard dynamically in get_vkeyboard(). Please correct me if I'm confused (which I undoubtedly am!).
Is there some other way to modify the appearance of the VKeyboard dynamically?
P.S. There seems to be a hint in Focus Behavior:
input_type is an OptionsProperty and defaults to ‘text’. Can be one of
‘text’, ‘number’, ‘url’, ‘mail’, ‘datetime’, ‘tel’ or ‘address’.
I added "input_type: 'number'" to a TextInput in .kv, but it didn't have any affect.
You can set keyboard class using Window.set_vkeyboard_class, but nothing prevents you from registering any function that returns a keyboard instance there, so you could use the context (your app's state) to decide which class will be used, dynamically. Since you are returning an instance yourself in this case, you can decide of its size, pos, and other details. I used this technique in a few app to use Animation on the instance to place it in a particular spot of the screen smoothly.
pseudo example:
from kivy.core.window import Window
from kivy.uix.vkeyboard import VKeyboard
from kivy.animation import Animation
from kivy.uix.screenmanager import ScreenManager, Screen
class KeyboardA(VKeyboard):
def place(self):
self.center_x = Window.center_x
self.top = 0
Animation(y=100, t='out_elastic', d=.4).start(self)
class KeyboardB(VKeyboard):
def place(self):
self.opacity = 0
Animation(opacity=1).start(self)
class MyApp(App):
def build(self):
sm = ScreenManger()
sm.add_widget(Screen(name='a'))
sm.add_widget(Screen(name='b'))
return sm
def get_keyboard(self, **kwargs):
if self.root.current == 'a':
kb = KeyboardA(**kwargs)
else:
kb = KeyboardB(**kwargs)
kb.place()
return kb
Window.set_vkeyboard_class(app.get_keyboard)
untested, but you should get the idea.

Size_hint and pos_hint of kivy not working when adding buttons programmatically in python

I am querying Mongolab for the number of documents in a collection, then I want to add one button per document found. I put this code in the on_enter function of a screen:
def on_enter(self):
topicconcepts = db.topicconcepts
topics = topicconcepts.distinct("topic")
for idx, topic in enumerate(topics):
button = Button(text=topic, id=str(idx), buttonPos = idx, size=(self.width,20), pos_hint = {'top': 1}, color= (1,1,1,1), halign='center', seq = 'pre')
# topicBox points to a BoxLayout
self.topicBox.add_widget(button)
However, the layout turns out to be this:
Basically I want the buttons to be more like this:
Any suggestions will be much appreciated :)
If you want to have fixed size buttons in a GridLayout (or a BoxLayout), you have to unset their size_hints. For example, for fixed height of 50dp, without changing width, it would be:
Button:
size_hint_y: None
height: '50dp'
on in py:
from kivy.metrics import dp
Button(size_hint_y=None, height=dp(50))
PS: remember, the buttons might overflow the screen, if too many topics are given. To correct this, use ScrollView widget to scroll them up and down.

kivy gridlayout child position after resizing window

im new to kivy , i'm trying to make an 8puzzle game, my problem is , after moving the numbers(using Buttons or labels) in a gridlayout (using animation calss to move the buttons) there is no problem till resizing the window ! after resizing window every button or label will be in it's initial position :-/
so,why gridlayout children come back to the initial position after ressizing the window ?
how can i fix this ?
python code :
class Cubes(Button):
#value=StringProperty('0')
pos_i=NumericProperty(0)
pos_j=NumericProperty(0)
class Puzzle(GridLayout):
def __init__(self,**kwargs):
super(Puzzle,self).__init__()
self._keyboard = Window.request_keyboard(None,self)
print("Tsfsdfsdfdsfsdfdsf")
if not self._keyboard:
return
game =[
[1,2,3],
[4,5,6],
[7,8,"-"]
]
for i in range (3):
for j in range (3):
self.add_widget(Cubes(text=str(game[i][j]),pos_i=i,pos_j=j))
self._keyboard.bind(on_key_down=self.move)
self.wait=False
self._index=-1
self._solution=[]
def move(self, keyboard, keycode, text,modifires):
pos=-1
for i in range(9):
if self.children[i].text=="-":
pos=self.children[i].pos
self._index=i
#print (self.children[self._index].pos)
if keycode[1]=="up":
if Expand(Node(self.generatePuzzleTable())).CanGoUp():
for m in range(9):
if self.children[m].pos_i==self.children[self._index].pos_i-1:
if self.children[m].pos_j==self.children[self._index].pos_j:
self.replace(self.children[m].pos,self.children[self._index].pos,m)
break
if keycode[1]=="right":
if Expand(Node(self.generatePuzzleTable())).CanGoRight:
for m in range(9):
if self.children[m].pos_i==self.children[self._index].pos_i:
if self.children[m].pos_j==self.children[self._index].pos_j+1:
self.replace(self.children[m].pos,self.children[self._index].pos,m)
break
if keycode[1]=="down":
if Expand(Node(self.generatePuzzleTable())).CanGoDown():
for m in range(9):
if self.children[m].pos_i==self.children[self._index].pos_i+1:
if self.children[m].pos_j==self.children[self._index].pos_j:
self.replace(self.children[m].pos,self.children[self._index].pos,m)
break
print (keycode[1])
if keycode[1]=="enter":
self.ans()
if keycode[1]=="left":
if Expand(Node(self.generatePuzzleTable())).CanGoLeft():
for m in range(9):
if self.children[m].pos_i==self.children[self._index].pos_i:
if self.children[m].pos_j==self.children[self._index].pos_j-1:
self.replace(self.children[m].pos,self.children[self._index].pos,m)
break
if keycode[1]=="spacebar":
print ("what the fuck ?!? ",keycode[1])
self.solve()
def replace(self,pos,pos1,NodeIndex):
anime=Animation(pos=(float(pos[0]),float(pos[1])),d=0.2,t="out_cubic")#.start(self.children[0])
anime.start(self.children[self._index])
anime2=Animation(pos=(float(pos1[0]),float(pos1[1])), d=0.2,t="out_cubic")#.start(self.children[1])
anime2.start(self.children[NodeIndex])
ti=self.children[self._index].pos_i
tj=self.children[self._index].pos_j
self.children[self._index].pos_i,self.children[self._index].pos_j=self.children[NodeIndex].pos_i,self.children[NodeIndex].pos_j
self.children[NodeIndex].pos_i,self.children[NodeIndex].pos_j=ti,tj
kivy code :
<Cubes>:
background_color: 1,1,1,1
<Puzzle>:
cols: 3
spacing: 2
size: (300, 300)
BoxLayout:
canvas.before:
Color:
rgb: (0.7, 0.3, .4)
Rectangle:
pos: (self.x, self.y)
size: (self.width, self.height)
size: (600, 600)
AnchorLayout:
Puzzle:
center_x: root.center_x
center_y: root.center_y*2/3
in this image i moved the dash button to top left , but i resize the window
it will be in it's initial position (bottom right)
GridLayout (or any Layout for that matter) is responsible for positioning its children widget so on each re-size (and other meaningful events) the layout will position the widgets from scratch (this is the behavior that annoys you).
In the case of GridLayout, what matters is the order of the children of the grid...
To fix your issue you have two two options:
1) after your animation is complete swap the "-" widget with other one on the grid children list.
2) use something like https://github.com/inclement/sparsegridlayout that lets you specify what is the (i,j) for each widget(grid entry)
I hope this makes things a bit more clear

Kivy layout positioning problems - not understanding/properly using __init__, kv, kv properties

I am a new programmer (and first time stackoverflow poster) so please correct me if I use terminology incorrectly or make any other missteps in etiquette or proper coding style.
I am trying to write a game where you draw tiles to your tile rack and then play them on a board. I have already written a game that works without graphics via text input. Now I would like to use Kivy to make a graphical interface.
One of my problems involves the positioning of widgets. I would like to center my rack widget at the center of the screen x-axis. I can have it draw a rectangle there and have it appear to be positioned where I want it, but its x position is (as you might guess) 0. I think part of my problem is that I have passed a Game object to my widget and using a list of symbols (game.symbols) and an init method, I tried to load create tile widgets with a label(text=symbol) and then load them on the rack. As you probably have guessed, my tiles also are not positioned correctly.
How can I center my tile rack and load my tiles correctly so they have the proper position (which I think is necessary for my collision detection).
Please explain the way init method and the KV file are executed when both are used.
What is the proper way to pass objects and attributes to widgets in regards to my issues here. Should I have created an ObjectProperty?
I also may just have a fundamental misunderstanding of positioning and layouts in Kivy and if so, please educate me.
Thank you,
Cliff
import kivy
kivy.require('1.7.0')
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.scatter import Scatter
from kivy.uix.label import Label
kv_Game= '''
<TileWidget>:
size_hint: None, None
size: 50,50
canvas.before:
Color:
rgba: 0.5,0.5,1,0.3
Rectangle:
size: self.width, self.height
canvas.after:
Line:
rectangle: self.x, self.y, self.width, self.height
dash_offset: 5
dash_length: 3
<RackWidget>:
size_hint: None, None
size: 350, 50
pos_hint: {'center_x': 0.5}
y: 75
canvas.after:
Color:
rgba: 1,0,0,0.5
Line:
rectangle: self.x, self.y, self.width, self.height
'''
Builder.load_string(kv_Game)
class Game(FloatLayout):
def __init__(self, **kwargs):
super(Game, self).__init__(**kwargs)
self.symbols = ['!','#','#','$','%','^','&']
self.rackWidget = RackWidget(self)
self.add_widget(self.rackWidget)
class TileWidget(Scatter):
def __init__(self, symbol="?", **kwargs):
super(TileWidget, self).__init__(**kwargs)
tileLabel = Label(text=symbol, size_hint=(None,None), size=(50,50))
self.add_widget(tileLabel)
class RackWidget(FloatLayout):
def __init__(self, game, **kwargs):
super(RackWidget, self).__init__(**kwargs)
print("TileRackWidget pos:", self.pos)
x, y = self.pos
for symbol in game.symbols:
tileWidget = TileWidget(symbol=symbol, pos= (x,y))
self.add_widget(tileWidget)
print("tileWidget pos:", tileWidget.pos)
x+=50
class GameTest1App(App):
def build(self):
game = Game()
return game
if __name__=="__main__":
GameTest1App().run()
pos is not set to a usable value yet when you create your RackWidget instance. When __init__ is running, the widget has not yet been added to the Game widget, so it has no parent and no positioning information. You could solve this by binding to the changes in RackWidget.pos, but there's an easier way to do this: RelativeLayout. The position of each child of a RelativeLayout will be based on the position of the layout itself.
Here's a modified version of RackWidget using RelativeLayout:
class RackWidget(RelativeLayout):
def __init__(self, game, **kwargs):
super(RackWidget, self).__init__(**kwargs)
print("TileRackWidget pos:", self.pos)
x, y = 0, 0
for symbol in game.symbols:
tileWidget = TileWidget(symbol=symbol, pos= (x,y))
self.add_widget(tileWidget)
print("tileWidget pos:", tileWidget.pos)
x+=50

Categories

Resources