Problem
I want to create multiple buttons and bind them to a function. The problem is that whenever I click on one button, the function is called multiple times. It seems to be a problem with the event connection. When I look at the instance that called the function when I pressed a button, it seems that the function gets called from every button at once?!
KV Code:
...
# This is the button that I'am using
<ProjectSelectButton>:
height: 35
background_color: 0,0,1,1
on_touch_down: self.click_on_button(args[0], args[1])
...
# The buttons are added to this grid
ButtonsPlacedHere#GridLayout:
id: active_projects
cols: 1
size_hint_y: None
height: self.minimum_height
spacing: 1
...
Python Code:
...
class ProjectSelectButton(Button):
def click_on_button(self, instance, touch, *args):
print(instance)
if touch.button == 'right':
print(self.id, "right mouse clicked")
else touch.buttom == 'left':
print(self.id, "left mouse clicked")
...
# The part of my programm that creates the buttons
# projects is a dictionary
for key, project in data_manager.resource_pool.projects.items():
print(project.project_name)
button= ProjectSelectButton(text=project.project_name, id=key, size_hint_y=None)
# Bind without KV-File (same result)
# label.bind(on_touch_up=self.click_on_button)
ids.active_projects.add_widget(button)
Example Output:
What I get, when I click on a single button!
<guiMain.ProjectSelectButton object at 0x0BA34260>
ID01 right mouse clicked
<guiMain.ProjectSelectButton object at 0x0BA34260>
ID01 right mouse clicked
<guiMain.ProjectSelectButton object at 0x0BA28F10>
ID02 right mouse clicked
<guiMain.ProjectSelectButton object at 0x0BA28F10>
ID02 right mouse clicked
<guiMain.ProjectSelectButton object at 0x0BA22C00>
ID03 right mouse clicked
<guiMain.ProjectSelectButton object at 0x0BA22C00>
ID03 right mouse clicked
What I want when I press for example the button with ID 01:
<guiMain.ProjectSelectButton object at 0x0BA34260>
ID01 right mouse clicked
Question
How do I create multiple buttons which will call a single function when they are pressed?
Programming Guide » Input management » Touch events
By default, touch events are dispatched to all currently displayed widgets. This means widgets receive the touch event whether it occurs within their physical area or not.
In order to provide the maximum flexibility, Kivy dispatches the
events to all the widgets and lets them decide how to react to them.
If you only want to respond to touch events inside the widget, you
simply check.
Solution
Use self.collide_point method in click_on_button method. When it collides, you should get only one button. Please refer to my example for details.
Snippets
class ProjectSelectButton(Button):
def click_on_button(self, instance, touch, *args):
print(instance)
if self.collide_point(*touch.pos):
if touch.button == 'right':
print(self.id, "right mouse clicked")
elif touch.buttom == 'left':
print(self.id, "left mouse clicked")
return True
return super(ProjectSelectButton, self).on_touch_down(touch)
Example
main.py
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button
class CreateButton(Button):
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
if touch.button == "right":
print(self.id, "right mouse clicked")
elif touch.button == "left":
print(self.id, "left mouse clicked")
else:
print(self.id)
return True
return super(CreateButton, self).on_touch_down(touch)
class OnTouchDownDemo(GridLayout):
def __init__(self, **kwargs):
super(OnTouchDownDemo, self).__init__(**kwargs)
self.build_board()
def build_board(self):
# make 9 buttons in a grid
for i in range(0, 9):
button = CreateButton(id=str(i))
self.add_widget(button)
class OnTouchDownApp(App):
def build(self):
return OnTouchDownDemo()
if __name__ == '__main__':
OnTouchDownApp().run()
ontouchdown.kv
#:kivy 1.10.0
<CreateButton>:
font_size: 50
on_touch_down: self.on_touch_down
<OnTouchDownDemo>:
rows: 3
cols: 3
row_force_default: True
row_default_height: 150
col_force_default: True
col_default_width: 150
padding: [10]
spacing: [10]
Related
is there any possibility in Kivy to make a button invisible, so that you run the on_press method of the button underneath when you click the first inviseible button?
Edit
class PlayGame(ButtonBehavior, Widget):
button = ObjectProperty(None)
def on_press(self):
do_something()
class PlayButton(Button):
def on_press(self):
if self.opacity == 1:
do_something()
elif self.opacity == 0:
return None
When I run this I can't click on the screen in the area where the button is (even when invisible) to run on_press() of PlayGame root widget.
Yes, make its opacity 0 and override its on_touch_down to do nothing (i.e. returning None, or you can return False if you like).
Desired result:
Currently my code is as follows:
class KeypadButton(Factory.Button):
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
FocusBehavior.ignored_touch.append(touch)
return super(KeypadButton, self).on_touch_down(touch)
class Keypad(Factory.GridLayout):
target = Factory.ObjectProperty(None, allownone=True)
def __init__(self, **kwargs):
super(Keypad, self).__init__(**kwargs)
self.cols = 3
for x in list(range(1, 10)) + ['<-', 0, 'Enter']:
btn = KeypadButton(text=str(x), on_release=self._on_release)
self.add_widget(btn)
def _on_focus(self, ti, value):
self.target = value and ti
def _on_release(self, instance, *largs):
if self.target:
if instance.text == 'Enter':
print("Enter: {}".format(self.target.text))
self.target.text = ''
elif instance.text == '<-':
if self.target.text:
self.target.text = self.target.text[:-1]
else:
self.target.text += str(instance.text)
runTouchApp(Builder.load_string('''
<KeypadTextInput#TextInput>:
keypad: None
on_focus: root.keypad._on_focus(*args)
BoxLayout:
orientation: 'vertical'
KeypadTextInput:
keypad: keypad
Keypad:
id: keypad
size_hint_x: 0.5
pos_hint: {'center_x': 0.5}
'''))
What I want to achieve is that when I press 12345 the login screen disappears and a new screen pop ups. I have an image of the format that is given by the following what I want to achieve.
Not sure how exactly how close you want to get to your desired result, but you can get much closer by just changing your 'kv' to:
<KeypadTextInput#TextInput>:
keypad: None
on_focus: root.keypad._on_focus(*args)
BoxLayout:
orientation: 'vertical'
KeypadTextInput:
keypad: keypad
Keypad:
id: keypad
size_hint_x: 0.5
pos_hint: {'center_x': 0.5}
First when it comes to the issue of focus, I think it depends on how you load up the screen. Is it your root widget or a screen you load up?
As yours looks to be a root widget of sorts you'd probably want to do this with the start up of your app. You can use the 'on_start' event for this
class MyApp(App):
def on_start(self,*args):
self.ids.mytextinput.focus = True #replace mytextinput with whatever id name you give to your text input in the kv string
For the text input firing off events when you type a certain number of digits you could use on_text. For this I think it's best to instantiate your own class if you're starting out.
class KeyPadTextInput(TextInput):
def on_text(self,*args):
if len(self.text)==3:
#put your python code here
#you can launch MyApp functions by using app.function_name()
Another thing I've noticed is that you use on_focus to trigger your own '_on_focus' event with the same *args. You could achieve the same thing by removing the on_focus from your kv string and adjusting the class on_focus event, calling super().on_focus(*args) so the inherited function also fires as such:
class KeyPadTextInput(TextInput):
def on_focus(self,*args):
#your code either before the super call
super().on_focus(*args)
#or your code after the super call
Hope that helps point you in the right direction.
PS. TextInputs have a few prebuilt input filters such as a filter so you can only input numbers! This is handy if the users keyboard comes up or they have access to one too.
in the kv string simply add
input_filter: 'int'
1. Objective
My purpose is to build a rightmouse-click-menu like this:
When the user clicks on Grab and move, the button should disappear from the QScrollArea() and move quickly towards the mouse. When it arrives at the mouse pointer, the button should fade out and the drag-and-drop operation can start.
2. Minimal, Reproducible Example
I got something working, but it isn't perfect yet. Please copy-paste the code below and run it with Python 3.x (I use Python 3.7) and PyQt5.
Note: To make the line pixmap = QPixmap("my_pixmap.png") work properly, let it refer to an existing png-image on your computer.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys
class MyButton(QPushButton):
'''
A special push button.
'''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFixedWidth(300)
self.setFixedHeight(30)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.showMenu)
return
def showMenu(self, pos):
'''
Show this popup menu when the user clicks with the right mouse button.
'''
menu = QMenu()
menuAction_01 = menu.addAction("action 01")
menuAction_02 = menu.addAction("action 02")
menuAction_03 = menu.addAction("action 03")
menuAction_04 = menu.addAction("action 04")
menuAction_grab = menu.addAction("grab")
action = menu.exec_(self.mapToGlobal(pos))
if action == menuAction_01:
print("clicked on action 01")
elif action == menuAction_02:
print("clicked on action 02")
elif action == menuAction_03:
print("clicked on action 03")
elif action == menuAction_04:
print("clicked on action 04")
elif action == menuAction_grab:
print("clicked on grab")
# 1. Start animation
# -> button moves to mouse pointer
self.animate()
# 2. After animation finishes (about 1 sec)
# -> start drag operation
QTimer.singleShot(1000, self.start_drag)
return
def animate(self):
'''
The button removes itself from the QScrollArea() and flies to the mouse cursor.
For more details, see the anser of #eyllanesc at
https://stackoverflow.com/questions/56216698/how-display-a-qpropertyanimation-on-top-of-the-qscrollarea
'''
startpoint = self.window().mapFromGlobal(self.mapToGlobal(QPoint()))
endpoint = self.window().mapFromGlobal(QCursor.pos())
self.setParent(self.window())
anim = QPropertyAnimation(
self,
b"pos",
self,
duration=1000,
startValue=startpoint,
endValue=endpoint,
finished=self.hide,
)
anim.start()
self.show()
return
def start_drag(self):
'''
Start the drag operation.
'''
drag = QDrag(self)
pixmap = QPixmap("my_pixmap.png")
pixmap = pixmap.scaledToWidth(100, Qt.SmoothTransformation)
drag.setPixmap(pixmap)
mimeData = QMimeData()
mimeData.setText("Foobar")
drag.setMimeData(mimeData)
dropAction = drag.exec(Qt.CopyAction | Qt.MoveAction)
return
class CustomMainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setGeometry(100, 100, 600, 300)
self.setWindowTitle("ANIMATION TEST")
# OUTER FRAME
# ============
self.frm = QFrame()
self.frm.setStyleSheet("""
QFrame {
background: #d3d7cf;
border: none;
}
""")
self.lyt = QHBoxLayout()
self.frm.setLayout(self.lyt)
self.setCentralWidget(self.frm)
# BUTTON FRAME
# =============
self.btn_frm = QFrame()
self.btn_frm.setStyleSheet("""
QFrame {
background: #ffffff;
border: none;
}
""")
self.btn_frm.setFixedWidth(400)
self.btn_frm.setFixedHeight(200)
self.btn_lyt = QVBoxLayout()
self.btn_lyt.setAlignment(Qt.AlignTop)
self.btn_lyt.setSpacing(5)
self.btn_frm.setLayout(self.btn_lyt)
# SCROLL AREA
# ============
self.scrollArea = QScrollArea()
self.scrollArea.setStyleSheet("""
QScrollArea {
border-style: solid;
border-width: 1px;
}
""")
self.scrollArea.setWidget(self.btn_frm)
self.scrollArea.setWidgetResizable(True)
self.scrollArea.setFixedWidth(400)
self.scrollArea.setFixedHeight(150)
self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.lyt.addWidget(self.scrollArea)
# ADD BUTTONS TO BTN_LAYOUT
# ==========================
self.btn_lyt.addWidget(MyButton("Foo"))
self.btn_lyt.addWidget(MyButton("Bar"))
self.btn_lyt.addWidget(MyButton("Baz"))
self.btn_lyt.addWidget(MyButton("Qux"))
self.show()
self.setAcceptDrops(True)
return
def dropEvent(self, event):
event.acceptProposedAction()
print("dropEvent at {0!s}".format(event))
return
def dragLeaveEvent(self, event):
event.accept()
return
def dragEnterEvent(self, event):
event.acceptProposedAction()
return
if __name__== '__main__':
app = QApplication(sys.argv)
QApplication.setStyle(QStyleFactory.create('Plastique'))
myGUI = CustomMainWindow()
sys.exit(app.exec_())
Run the script and you will see a small window with a few buttons in a QScrollArea():
STEP 1: Click on one of the buttons with your right mouse button. You should see a popup menu. Click on "grab".
STEP 2: The button moves to your mouse pointer. Don't move the mouse pointer.
STEP 3: As soon as your mouse pointer is over the button (don't move the mouse, wait for the button to arrive), click and hold the mouse button down.
STEP 4: Now move the mouse (while holding the mouse button down). You should be in a drag-and-drop operation, with the pixmap locked to your mouse!
Okay, it works, but there are a few downsides.
3. Problem
At the end of the animation, the flying button is under your mouse pointer. But if you move your mouse pointer a tiny bit, the button disappears and you miss the drag-and-drop operation.
In other words, what I got now is not very robust. The user can easily miss the drag-and-drop operation.
NOTE: Apparently the problem I describe here only appears on Windows (not on Linux). But I got to make this thing work on Windows...
4. Potential solution
I believe the following approach would be better, and still intuitive to the user:
As soon as the button arrives under the mouse pointer (the end of the animation), the button fades away. The drag-and-drop operation starts automatically, without the need to click and hold down the mouse button. The drag continues while you move the mouse pointer, until you click somewhere. That mouse press is the dropEvent().
Do you know how to implement this? Or perhaps you have another approach in mind?
5. Notes
My question is actually the sequel of this one:
How display a QPropertyAnimation() on top of the QScrollArea()?
Thank you #eyllanesc for solving that one ^_^
1. Solution
Before presenting a solution, I want to express my gratitude to Mr. #eyllanesc for helping me. Without his help, I wouldn't have a solution right now.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys, functools
class MyButton(QPushButton):
'''
A special push button.
'''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFixedWidth(300)
self.setFixedHeight(30)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.showMenu)
self.dragStartPosition = 0
self.set_style(False)
return
def set_style(self, blink):
if blink:
background = "#d3d7cf"
else:
background = "#2e3436"
self.setStyleSheet(f"""
QPushButton {{
/* white on red */
background-color:{background};
color:#ffffff;
border-color:#888a85;
border-style:solid;
border-width:1px;
border-radius: 6px;
font-family:Courier;
font-size:10pt;
padding:2px 2px 2px 2px;
}}
""")
self.update()
return
def showMenu(self, pos):
'''
Show this popup menu when the user clicks with the right mouse button.
'''
menu = QMenu()
menuAction_01 = menu.addAction("action 01")
menuAction_02 = menu.addAction("action 02")
menuAction_03 = menu.addAction("action 03")
menuAction_04 = menu.addAction("action 04")
menuAction_grab = menu.addAction("grab")
action = menu.exec_(self.mapToGlobal(pos))
if action == menuAction_01:
print("clicked on action 01")
elif action == menuAction_02:
print("clicked on action 02")
elif action == menuAction_03:
print("clicked on action 03")
elif action == menuAction_04:
print("clicked on action 04")
elif action == menuAction_grab:
print("clicked on grab")
# Start animation -> button moves to mouse pointer
self.animate()
return
def animate(self):
'''
The button removes itself from the QScrollArea() and flies to the mouse cursor.
For more details, see the anser of #eyllanesc at
https://stackoverflow.com/questions/56216698/how-display-a-qpropertyanimation-on-top-of-the-qscrollarea
'''
def start():
startpoint = self.window().mapFromGlobal(self.mapToGlobal(QPoint()))
endpoint = self.window().mapFromGlobal(QCursor.pos() - QPoint(int(self.width()/2), int(self.height()/2)))
self.setParent(self.window())
anim = QPropertyAnimation(
self,
b"pos",
self,
duration=500,
startValue=startpoint,
endValue=endpoint,
finished=blink,
)
anim.start()
self.show()
return
def blink():
# Flash the button to catch attention
self.setText("GRAB ME")
QTimer.singleShot(10, functools.partial(self.set_style, True))
QTimer.singleShot(100, functools.partial(self.set_style, False))
QTimer.singleShot(200, functools.partial(self.set_style, True))
QTimer.singleShot(300, functools.partial(self.set_style, False))
QTimer.singleShot(400, functools.partial(self.set_style, True))
QTimer.singleShot(500, functools.partial(self.set_style, False))
finish()
return
def finish():
# After two seconds, hide the button
# (even if user did not grab it)
QTimer.singleShot(2000, self.hide)
return
start()
return
def start_drag(self):
'''
Start the drag operation.
'''
# 1. Start of drag-and-drop operation
# => button must disappear
self.hide()
# 2. Initiate drag-and-drop
drag = QDrag(self)
pixmap = QPixmap("my_pixmap.png")
pixmap = pixmap.scaledToWidth(100, Qt.SmoothTransformation)
drag.setPixmap(pixmap)
mimeData = QMimeData()
mimeData.setText("Foobar")
drag.setMimeData(mimeData)
dropAction = drag.exec(Qt.CopyAction | Qt.MoveAction)
return
def mousePressEvent(self, event):
'''
Left or Right mouseclick
'''
def leftmouse():
print("left mouse click")
self.dragStartPosition = event.pos()
return
def rightmouse():
print("right mouse click")
return
if event.button() == Qt.LeftButton:
leftmouse()
return
if event.button() == Qt.RightButton:
rightmouse()
return
return
def mouseMoveEvent(self, event):
'''
Mouse move event
'''
event.accept()
if event.buttons() == Qt.NoButton:
return
if self.dragStartPosition is None:
return
if (event.pos() - self.dragStartPosition).manhattanLength() < QApplication.startDragDistance():
return
self.start_drag()
return
class CustomMainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setGeometry(100, 100, 600, 300)
self.setWindowTitle("ANIMATION & DRAG AND DROP")
# OUTER FRAME
# ============
self.frm = QFrame()
self.frm.setStyleSheet("""
QFrame {
background: #d3d7cf;
border: none;
}
""")
self.lyt = QHBoxLayout()
self.frm.setLayout(self.lyt)
self.setCentralWidget(self.frm)
# BUTTON FRAME
# =============
self.btn_frm = QFrame()
self.btn_frm.setStyleSheet("""
QFrame {
background: #ffffff;
border: none;
}
""")
self.btn_frm.setFixedWidth(400)
self.btn_frm.setFixedHeight(200)
self.btn_lyt = QVBoxLayout()
self.btn_lyt.setAlignment(Qt.AlignTop)
self.btn_lyt.setSpacing(5)
self.btn_frm.setLayout(self.btn_lyt)
# SCROLL AREA
# ============
self.scrollArea = QScrollArea()
self.scrollArea.setStyleSheet("""
QScrollArea {
border-style: solid;
border-width: 1px;
}
""")
self.scrollArea.setWidget(self.btn_frm)
self.scrollArea.setWidgetResizable(True)
self.scrollArea.setFixedWidth(400)
self.scrollArea.setFixedHeight(150)
self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.lyt.addWidget(self.scrollArea)
# ADD BUTTONS TO BTN_LAYOUT
# ==========================
self.btn_lyt.addWidget(MyButton("Foo"))
self.btn_lyt.addWidget(MyButton("Bar"))
self.btn_lyt.addWidget(MyButton("Baz"))
self.btn_lyt.addWidget(MyButton("Qux"))
self.show()
self.setAcceptDrops(True)
return
def dropEvent(self, event):
event.acceptProposedAction()
print("dropEvent at {0!s}".format(event))
return
def dragLeaveEvent(self, event):
event.accept()
return
def dragEnterEvent(self, event):
event.acceptProposedAction()
return
if __name__== '__main__':
app = QApplication(sys.argv)
QApplication.setStyle(QStyleFactory.create('Plastique'))
myGUI = CustomMainWindow()
sys.exit(app.exec_())
This is what I changed:
I improved the animation. Not the top left corner but the middle of the button flies to your mouse pointer when you click "grab" in the rightmouse menu. This improvement makes it much easier to grab the button once the animation has finished.
At the end of the animation, the button flashes for a short moment to catch the user's attention. The text on the button changes into "GRAB ME". The button's self.hide() function is NOT called for two seconds. So the user has two seconds time to initiate a drag-and-drop operation.
Initiating the drag-and-drop operation happens in the usual way: hold down the leftmouse button and move the mouse pointer.
If the user doesn't do anything for two seconds, the button will just disappear. Otherwise, the button would just sit there indefinitely.
2. Results
It works like a charm. Just copy past the code in a .py file and run it with Python 3.x (I got Python 3.7) and PyQt5:
3. Conclusion
I know that this solution is not exactly what I aimed at from the beginning: performing a drag-and-drop without holding down the mouse button. Nevertheless, I think the new approach is better because it's closer to the general intuition what a drag-and-drop actually is.
I'm using a for loop to generate a button for every key in a jsonstore.
The buttons are all added to the layout correctly and the button.text is correct but when the button calls the callback, they all link to the same key. (the last key)
here is the code:
def show_saved_conditions(*args,**kwargs):
self.label7 = Label(text = str(self.store[self.btns.text], size_hint = (.3,.3), pos_hint = {'x':.3,'y':.5}, color = (0,0,1,1)))
self.layout.add_widget(self.label7)
def view_saved_conditions(*args, **kwargs):
x = 0
y = 0
for i in self.store.keys():
self.btns = (Button(text = i, size_hint = (.2,.1), pos_hint = {'x':x,'y':y}, on_release = show_saved_conditions))
self.layout.add_widget(self.btns)
x +=.2
if x >= 1:
y+=.1
x = 0
pretty sure this question has been asked before but i was unable to find a post specific enough for me to relate to.
Thank you in advance...
Problem - Always refer to the last button
In show_saved_conditions() method, it is always using self.btns.text which is the last button added to the layout.
self.label7 = Label(text = str(self.store[self.btns.text], size_hint = (.3,.3), pos_hint = {'x':.3,'y':.5}, color = (0,0,1,1)))
Solution
In the example, it demonstrates on_touch_down event.
Touch event basic
By default, touch events are dispatched to all currently displayed
widgets. This means widgets receive the touch event whether it occurs
within their physical area or not.
In order to provide the maximum flexibility, Kivy dispatches the
events to all the widgets and lets them decide how to react to them.
If you only want to respond to touch events inside the widget, you
simply check:
def move(self, touch):
if self.collide_point(*touch.pos):
# The touch has occurred inside the widgets area. Do stuff!
pass
Example
main.py
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button
class CreateButton(Button):
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
if touch.button == "right":
print(self.id, "right mouse clicked")
elif touch.button == "left":
print(self.id, "left mouse clicked")
else:
print(self.id)
return True
return super(CreateButton, self).on_touch_down(touch)
class OnTouchDownDemo(GridLayout):
def __init__(self, **kwargs):
super(OnTouchDownDemo, self).__init__(**kwargs)
self.build_board()
def build_board(self, *args):
# make 9 buttons in a grid
for i in range(0, 9):
button = CreateButton(id=str(i), text=str(i))
self.add_widget(button)
class OnTouchDownApp(App):
def build(self):
return OnTouchDownDemo()
if __name__ == '__main__':
OnTouchDownApp().run()
ontouchdown.kv
#:kivy 1.11.0
<CreateButton>:
font_size: 50
<OnTouchDownDemo>:
rows: 3
cols: 3
row_force_default: True
row_default_height: 150
col_force_default: True
col_default_width: 150
padding: [10]
spacing: [10]
Output
The *args argument to your show_saved_conditions() method contains the Button instance that was pressed. So that method could be:
def show_saved_conditions(self, btn_instance):
self.label7 = Label(text = str(self.store[btn_instance.text], size_hint = (.3,.3), pos_hint = {'x':.3,'y':.5}, color = (0,0,1,1)))
self.layout.add_widget(self.label7)
Since you used self in the method, I have assumed that this is an instance method and the correct first arg is self. If it is not an instance method, then just remove the self arg, but then where does the method get the value for self?
Of course, this method will overwrite self.label7 each time that it is executed.
I'm trying have a grid layout, that I can have on item foucsed, and I can navigate using the arrow keys (up/down and left/right).
the idea is for a leanback expireance application (i.e. android tv, or a home media center), with no touch or mouse.
I'm trying to reuse the FocusBehavior and CompoundSelectionBehavior for that
I have something that is almost there, but I can't figure out how do I shift the selection to the next row, left/right keep on the first row that I've click with the mouse, and doesn't move.
from kivy.uix.behaviors.compoundselection import CompoundSelectionBehavior
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.behaviors import FocusBehavior
from kivy.app import App
class SelectableBoxLayout(FocusBehavior, CompoundSelectionBehavior, BoxLayout):
def keyboard_on_key_down(self, window, keycode, text, modifiers):
"""Based on FocusBehavior that provides automatic keyboard
access, key presses will be used to select children.
"""
print(keycode, text, modifiers)
print(self.orientation)
if super(SelectableBoxLayout, self).keyboard_on_key_down(
window, keycode, text, modifiers):
return True
if self.orientation == 'horizontal' and keycode[1] in ['up', 'down']:
self.clear_selection()
return self.parent.keyboard_on_key_down(window, keycode, text, modifiers)
if self.select_with_key_down(window, keycode, text, modifiers):
return True
return False
def keyboard_on_key_up(self, window, keycode):
"""Based on FocusBehavior that provides automatic keyboard
access, key release will be used to select children.
"""
if super(SelectableBoxLayout, self).keyboard_on_key_up(window, keycode):
return True
if self.orientation == 'horizontal' and keycode[1] in ['up', 'down']:
self.clear_selection()
return self.parent.keyboard_on_key_up(window, keycode)
if self.select_with_key_up(window, keycode):
return True
return False
def add_widget(self, widget):
""" Override the adding of widgets so we can bind and catch their
*on_touch_down* events. """
widget.bind(on_touch_down=self.button_touch_down,
on_touch_up=self.button_touch_up)
return super(SelectableBoxLayout, self).add_widget(widget)
def button_touch_down(self, button, touch):
""" Use collision detection to select buttons when the touch occurs
within their area. """
if button.collide_point(*touch.pos):
self.select_with_touch(button, touch)
def button_touch_up(self, button, touch):
""" Use collision detection to de-select buttons when the touch
occurs outside their area and *touch_multiselect* is not True. """
if not (button.collide_point(*touch.pos) or
self.touch_multiselect):
self.deselect_node(button)
def select_node(self, node):
node.background_color = (1, 0, 0, 1)
return super(SelectableBoxLayout, self).select_node(node)
def deselect_node(self, node):
node.background_color = (1, 1, 1, 1)
super(SelectableBoxLayout, self).deselect_node(node)
def on_selected_nodes(self, gird, nodes):
print("Selected nodes = {0}".format(nodes))
if self.orientation == 'vertical':
if nodes:
row = nodes[0]
row.clear_selection()
node_src, idx_src = row._resolve_last_node()
text = 'left'
node, idx = row.goto_node(text, node_src, idx_src)
row.select_node(node)
class TestApp(App):
def build(self):
grid = SelectableBoxLayout(orientation='vertical', touch_multiselect=False,
multiselect=False)
for i in range(0, 6):
row = SelectableBoxLayout(orientation='horizontal', touch_multiselect=False,
multiselect=False)
for j in range(0,5):
b = Button(text="Button {0}".format(j))
row.add_widget(b)
grid.add_widget(row)
row.get_focus_next().focus = True
return grid
TestApp().run()
Took me a while debugging, but here's a working example:
from kivy.uix.behaviors.compoundselection import CompoundSelectionBehavior
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.behaviors import FocusBehavior
from kivy.app import App
class SelectableBoxLayout(FocusBehavior, CompoundSelectionBehavior, BoxLayout):
def keyboard_on_key_down(self, window, keycode, text, modifiers):
"""Based on FocusBehavior that provides automatic keyboard
access, key presses will be used to select children.
"""
print(keycode, text, modifiers)
print(self.orientation)
if super(SelectableBoxLayout, self).keyboard_on_key_down(
window, keycode, text, modifiers):
return True
if self.orientation == 'horizontal' and keycode[1] in ['up', 'down']:
self.clear_selection()
return self.parent.keyboard_on_key_down(window, keycode, text, modifiers)
if self.orientation == 'vertical' and keycode[1] in ['up', 'down']:
direction = 'focus_next' if keycode[1] == 'down' else 'focus_previous'
if self.selected_nodes:
next_row = self.selected_nodes[0]._get_focus_next(direction)
else:
next_row = self.children[-1]
self.clear_selection()
self.select_node(next_row)
if next_row:
next_row.focus = True
next = next_row.children[-1]
if next and not isinstance(next, SelectableBoxLayout):
print("moving to {0}".format(next))
next.focus = True
next_row.clear_selection()
next_row.select_node(next)
return True
if self.select_with_key_down(window, keycode, text, modifiers):
return True
return False
def keyboard_on_key_up(self, window, keycode):
"""Based on FocusBehavior that provides automatic keyboard
access, key release will be used to select children.
"""
if super(SelectableBoxLayout, self).keyboard_on_key_up(window, keycode):
return True
if self.orientation == 'horizontal' and keycode[1] in ['up', 'down']:
return self.parent.keyboard_on_key_up(window, keycode)
if self.select_with_key_up(window, keycode):
return True
return False
def add_widget(self, widget, index=0):
""" Override the adding of widgets so we can bind and catch their
*on_touch_down* events. """
widget.bind(on_touch_down=self.button_touch_down,
on_touch_up=self.button_touch_up)
return super(SelectableBoxLayout, self).add_widget(widget, index)
def button_touch_down(self, button, touch):
""" Use collision detection to select buttons when the touch occurs
within their area. """
if button.collide_point(*touch.pos):
self.select_with_touch(button, touch)
def button_touch_up(self, button, touch):
""" Use collision detection to de-select buttons when the touch
occurs outside their area and *touch_multiselect* is not True. """
if not (button.collide_point(*touch.pos) or
self.touch_multiselect):
self.deselect_node(button)
def select_node(self, node):
node.background_color = (1, 0, 0, 1)
print("select: {}".format(getattr(node, 'text', 'none')))
return super(SelectableBoxLayout, self).select_node(node)
def deselect_node(self, node):
node.background_color = (1, 1, 1, 1)
print("deselect: {}".format(getattr(node, 'text', 'none')))
super(SelectableBoxLayout, self).deselect_node(node)
def on_selected_nodes(self, grid, nodes):
pass
class TestingappApp(App):
"""Basic kivy app
Edit testingapp.kv to get started.
"""
def build(self):
grid = SelectableBoxLayout(orientation='vertical', touch_multiselect=False,
multiselect=False)
for i in range(0, 6):
row = SelectableBoxLayout(orientation='horizontal', touch_multiselect=False,
multiselect=False)
for j in range(0,5):
b = Button(text="Event A\n TT {}{}".format(i, j))
row.add_widget(b)
grid.add_widget(row)
row.get_focus_next().focus = True
return grid
Like with the original kivy sample the first item is not shown with the selection color after starting the program. To change this store the item-objekt for the first item (if i and j == 0) and behind the loop call select_node(first_item).
With the original sample (at least in my environment) after starting the program there is no reaction when pressing ANY button on the keyboard. As soon as there was a mouse click e.g. pressing a button, then the keyboard also works. In the above sample the keyboard can be used without prior mouse click.
Calling selectable_object.focus=true was the secret.