I have a textinput that I want to focus on when the user clicks/touches on it. (Fairly standard!) It inherits from DragableObject (a user example in the kivy wiki) and GridLayout.
class DragableObject( Widget ):
def on_touch_down( self, touch ):
if self.collide_point( *touch.pos ):
touch.grab( self )
return True
def on_touch_up( self, touch ):
if touch.grab_current is self:
touch.ungrab( self )
return True
def on_touch_move( self, touch ):
if touch.grab_current is self:
self.pos = touch.x-self.width/2, touch.y-self.height/2
class MyWidget(DragableObject, GridLayout):
def __init__(self, **kwargs):
kwargs['orientation'] = 'lr-tb'
kwargs['spacing'] = 10
kwargs['size_hint'] = (None, None)
kwargs['size'] = (200, 80)
self.cols = 2
super(MyWidget, self).__init__(**kwargs)
with self.canvas:
self.rect = Rectangle(pos=self.pos, size=self.size)
with self.canvas.before:
Color(0, .5, 1, mode='rgb')
self.bind(pos=self.update_rect)
self.bind(size=self.update_rect)
self.add_widget(Label(text='t1'))
self.text1 = TextInput(multiline=False)
self.add_widget(self.text1)
self.add_widget(Label(text='t2'))
self.text2 = TextInput(multiline=False)
self.add_widget(self.text2)
# these bind's don't seem to work
self.text1.bind(on_touch_down=self.set_focus)
self.text1.bind(on_touch_up=self.set_focus)
self.text1.bind(on_touch_move=self.set_focus)
def set_focus(self):
print("pressed")
self.text1.focus = True
def update_rect(self, *args):
self.rect.pos = self.pos
self.rect.size = self.size
I have two problems.
a. The text input is unfocusable.
b. I can't get an event callback (such as on_touch_down) to work on the textinput widget.
Any ideas?
Short Answer
You can simple use an Scatter. It includes dragging, rotation and scaling functionality and you can deactivate any or all of them:
my_scatter = Scatter(do_rotation=False, do_scale=False)
None of the problems you described should happen inside a Scatter
Long Answer
Your problem is that you are overriding the on_touch_down, on_touch_move and on_touch_up of the parent.
Normally these methods will call the corresponding ones of the children. For example, when the on_touch_down method of a Widget instance is called, then the Widget instance is going to traverse its children, calling the on_touch_down method of each of then (If you are familiarized with recursion and tree structures, we are talking about a recursive traverse method - I think pre-order - Tree traversal).
This functionality is overridden in the DraggableObject class. You got to be sure to call the method of be base class with:
super(DraggableWidget, self).on_touch_down(touch)
Depending on what is the behaviour you are looking for the methods could look like:
(1) If you always want to call the children:
def on_touch_down( self, touch ):
if self.collide_point( *touch.pos ):
touch.grab( self )
return super(DraggableWidget, self).on_touch_down(touch)
(2) If you just want to call the children when there is no collide:
def on_touch_down( self, touch ):
if self.collide_point( *touch.pos ):
touch.grab( self )
return True # Don't call the on_touch_down of the Base Class
return super(DraggableWidget, self).on_touch_down(touch)
And there is more options!. The return value indicates if the event was or not handled by any children. You can, for example, do something like this:
def on_touch_down( self, touch ):
handled = super(DraggableWidget, self).on_touch_down(touch)
if not handled and self.collide_point( *touch.pos ):
touch.grab( self )
return True
return handled
In this case, you will avoid the Widget to be dragged when one of the children handle the event. It all depends on what you do.
Related
I am trying to a make a music player using Kivy, which will just have two button; one for play and one for pause. When I trying to bind a function to the buttons, when I press the buttons, I get errors. Upon searching this very own site for this, there was a suggestion to use the partial function from the functools library. But even that didn't work. My code is below.
class Layout(FloatLayout):
def __init__(self, **kwargs):
super(Layout, self).__init__(**kwargs)
self.size = Window.size
self.play = Button(text="Play", size_hint=(0.25, 0.25), font_size=36, background_color=color,
pos=(self.size[0]*(3/8), self.size[1]*(4/10)) )
self.pause = Button(text="Pause", size_hint=(0.25, 0.25), font_size=36, background_color=color,
pos=(self.size[0]*(3/8), self.size[1]*(1/10)) )
self.play.bind(on_press=self.play)
self.pause.bind(on_press=self.pause)
self.add_widget(self.play)
self.add_widget(self.pause)
def play(self):
print("PLay")
def pause(self):
print("Pause")
This gave this error
AssertionError: <kivy.uix.button.Button object at 0x000001D1D4792CF0>
is not callable
and by using the partial function
self.play.bind(on_press=partial(self.play))
self.pause.bind(on_press=partial(self.pause))
I get served this
TypeError: the first argument must be callable
There are two errors in your code. the first one is that you have given the event handlers and the buttons the same name. So change one of those. Second, you need a second parameter to event handlers as it gets called with a parameter.
Here is your corrected code:
class Layout(FloatLayout):
def __init__(self, **kwargs):
super(Layout, self).__init__(**kwargs)
self.size = Window.size
self.play = Button(text="Play", size_hint=(0.25, 0.25), font_size=36,
pos=(self.size[0]*(3/8), self.size[1]*(4/10)) )
self.pause = Button(text="Pause", size_hint=(0.25, 0.25), font_size=36,
pos=(self.size[0]*(3/8), self.size[1]*(1/10)) )
self.play.bind(on_press=self.play1)
self.pause.bind(on_press=self.pause1)
self.add_widget(self.play)
self.add_widget(self.pause)
def play1(self, instance):
print("PLay")
def pause1(self, instance):
print("Pause")
If you don't want to add a parameter then you can use lambda function.
Something like this:
self.play.bind(on_press=lambda _:self.play1())
self.pause.bind(on_press=lambda _:self.pause1())
In this case, you can remove the extra parameter in the eventhandler.
I am using PyQt and I'm trying to re-implement a QGraphicsTextItem, but it seems I'm missing something.
I would like to make the NodeTag item's text editable. I have tried setting flags such as Qt.TextEditorInteraction and QGraphicsItem.ItemIsMovable , but those seem to be ignored...
Here is a Minimal Reproducible Example :
import sys
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView, QMainWindow, QApplication, QGraphicsItem, QGraphicsTextItem
from PyQt5.QtCore import *
from PyQt5.QtGui import QPen
class NodeTag(QGraphicsTextItem):
def __init__(self,text):
QGraphicsTextItem.__init__(self,text)
self.text = text
self.setPos(0,0)
self.setTextInteractionFlags(Qt.TextEditorInteraction)
# self.setFlag(QGraphicsItem.ItemIsFocusable, True) # All these flags are ignored...
# self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
def boundingRect(self):
return QRectF(0,0,80,25)
def paint(self,painter,option,widget):
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
painter.drawText(self.boundingRect(),self.text)
def mousePressEvent(self, event):
print("CLICK!")
# self.setTextInteractionFlags(Qt.TextEditorInteraction) # make text editable on click
# self.setFocus()
class GView(QGraphicsView):
def __init__(self, parent, *args, **kwargs):
super().__init__(*args, **kwargs)
self.parent = parent
self.setGeometry(100, 100, 700, 450)
self.show()
class Scene(QGraphicsScene):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
tagItem = NodeTag("myText") # create a NodeTag item
self.addItem(tagItem)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__() # create default constructor for QWidget
self.setGeometry(900, 70, 1000, 800)
self.createGraphicView()
self.show()
def createGraphicView(self):
self.scene = Scene(self)
gView = GView(self)
scene = Scene(gView)
gView.setScene(scene)
# Set the main window's central widget
self.setCentralWidget(gView)
# Run program
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec_())
As you can see I have tried overriding the mousePressEvent and setting flags there too, but no luck so far.
Any help appreciated!
All QGraphicsItem subclasses have a paint method, and all items that paint some contents have that method overridden so that they can actually paint themselves.
The mechanism is the same as standard QWidgets, for which there is a paintEvent (the difference is that paint of QGraphicsItem receives an already instanciated QPainter), so if you want to do further painting other than what the class already provides, the base implementation must be called.
Consider that painting always happen from bottom to top, so everything that needs to be drawn behind the base painting has to be done before calling super().paint(), and everything that is going to be drawn in front of the default painting has to be placed after.
Depending on the situation, overriding might require that the default base implementation is called anyway, and that's important in your case for boundingRect too. QGraphicsTextItem automatically resizes itself when its contents change, so you should not always return a fixed QRect. If you need to have a minimum size, the solution is to merge a minimum rectangle with those provided by the default boundingRect() function.
Then, editing on a QGraphicsTextItem happens when the item gets focused, but since you also want to be able to move the item, things get trickier as both actions are based on mouse clicks. If you want to be able to edit the text with a single click, the solution is to make the item editable only when the mouse button has been released and has not been moved by some amount of pixels (the startDragDistance() property), otherwise the item is moved with the mouse. This obviously makes the ItemIsMovable flag useless, as we're going to take care of the movement internally.
Finally, since a minimum size is provided, we also need to override the shape() method in order to ensure that collision and clicks are correctly mapped, and return a QPainterPath that includes the whole bounding rect (for normal QGraphicsItem that should be the default behavior, but that doesn't happen with QGraphicsRectItem).
Here's a full implementation of what described above:
class NodeTag(QGraphicsTextItem):
def __init__(self, text):
QGraphicsTextItem.__init__(self, text)
self.startPos = None
self.isMoving = False
# the following is useless, not only because we are leaving the text
# painting to the base implementation, but also because the text is
# already accessible using toPlainText() or toHtml()
#self.text = text
# this is unnecessary too as all new items always have a (0, 0) position
#self.setPos(0, 0)
def boundingRect(self):
return super().boundingRect() | QRectF(0, 0, 80, 25)
def paint(self, painter, option, widget):
# draw the border *before* (as in "behind") the text
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
super().paint(painter, option, widget)
def shape(self):
shape = QPainterPath()
shape.addRect(self.boundingRect())
return shape
def focusOutEvent(self, event):
# this is required in order to allow movement using the mouse
self.setTextInteractionFlags(Qt.NoTextInteraction)
def mousePressEvent(self, event):
if (event.button() == Qt.LeftButton and
self.textInteractionFlags() != Qt.TextEditorInteraction):
self.startPos = event.pos()
else:
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.startPos:
delta = event.pos() - self.startPos
if (self.isMoving or
delta.manhattanLength() >= QApplication.startDragDistance()):
self.setPos(self.pos() + delta)
self.isMoving = True
return
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if (not self.isMoving and
self.textInteractionFlags() != Qt.TextEditorInteraction):
self.setTextInteractionFlags(Qt.TextEditorInteraction)
self.setFocus()
# the following lines are used to correctly place the text
# cursor at the mouse cursor position
cursorPos = self.document().documentLayout().hitTest(
event.pos(), Qt.FuzzyHit)
textCursor = self.textCursor()
textCursor.setPosition(cursorPos)
self.setTextCursor(textCursor)
super().mouseReleaseEvent(event)
self.startPos = None
self.isMoving = False
As a side note, remember that QGraphicsTextItem supports rich text formatting, so even if you want more control on the text painting process you should not use QPainter.drawText(), because you'd only draw the plain text. In fact, QGraphicsTextItem draws its contents using the drawContents() function of the underlying text document.
Try it:
...
class NodeTag(QGraphicsTextItem):
def __init__(self, text, parent=None):
super(NodeTag, self).__init__(parent)
self.text = text
self.setPlainText(text)
self.setFlag(QGraphicsItem.ItemIsMovable)
self.setFlag(QGraphicsItem.ItemIsSelectable)
def focusOutEvent(self, event):
self.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
super(NodeTag, self).focusOutEvent(event)
def mouseDoubleClickEvent(self, event):
if self.textInteractionFlags() == QtCore.Qt.NoTextInteraction:
self.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction)
super(NodeTag, self).mouseDoubleClickEvent(event)
def paint(self,painter,option,widget):
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
# painter.drawText(self.boundingRect(),self.text)
super().paint(painter, option, widget)
...
In Kivy manual is example for using FileChooser with kivy language. I want to use FileChooser only in python code. When I mark directory by mouse, press button Select Dir and actually value is in variable FileChooser.path. Selection without using this button has not result.
In kv file from example is used event on_selection, I binded this event with my function, but without effect.
My questions:
How do I get value to path with only mouse using?
Which class use event on_selection?
Thank You!
class Explorer(BoxLayout):
def __init__(self, **kwargs):
super(Explorer,self).__init__(**kwargs)
self.orientation = 'vertical'
self.fichoo = FileChooserListView(size_hint_y = 0.8)
self.add_widget(self.fichoo)
control = GridLayout(cols = 5, row_force_default=True, row_default_height=35, size_hint_y = 0.14)
lbl_dir = Label(text = 'Folder',size_hint_x = None, width = 80)
self.tein_dir = TextInput(size_hint_x = None, width = 350)
bt_dir = Button(text = 'Select Dir',size_hint_x = None, width = 80)
bt_dir.bind(on_release =self.on_but_select)
self.fichoo.bind(on_selection = self.on_mouse_select)
control.add_widget(lbl_dir)
control.add_widget(self.tein_dir)
control.add_widget(bt_dir)
self.add_widget(control)
return
def on_but_select(self,obj):
self.tein_dir.text = str(self.fichoo.path)
return
def on_mouse_select(self,obj):
self.tein_dir.text = str(self.fichoo.path)
return
def on_touch_up(self, touch):
self.tein_dir.text = str(self.fichoo.path)
return super().on_touch_up(touch)
return super().on_touch_up(touch)
Few changes need to be done.
There's no such event on_selection, there's property selection in FileChooserListView. You can use functions on_<propname> inside class that has these property, but when you use bind, you should use only bind(<propname>=.
Second thing is that by default as you can see in the doc selection contains list of selected files, not directories. To make directories actually selctable, you should change dirselect property to True.
Last on is on_mouse_select signature: property fires with it's value, you should count that.
Summary changes are:
self.fichoo.dirselect = True
self.fichoo.bind(selection = self.on_mouse_select)
# ... and
def on_mouse_select(self, obj, val):
After that you would be do the same as with button.
If you want to fill input with not actually path you're in, but selected path, you can do following:
def on_touch_up(self, touch):
if self.fichoo.selection:
self.tein_dir.text = str(self.fichoo.selection[0])
return super().on_touch_up(touch)
test.kv
<RootWidget>:
test: test
Label:
id: test
text: "some"
test.py
class RootWidget(Widget):
test = ObjectProperty(None)
def __init__(self, **kwargs):
# 1:
print(self.test) # None
# 2:
def after_tick(*args):
print(self.test) # Label object
Clock.schedule_once(after_tick, 0)
super().__init__()
If I'll try to bind something to self.test directly inside __init__ (1), I would get AttributeError since kv rules aren't applied yet and self.test is None. Possible workaround would be to bind to attribute after first event loop iteration (2).
This solution seems to be duct tape. Is there any better way?
I know 3 other ways.
First, make the after_tick a method and execute it in init:
class RootWidget(Widget):
test = ObjectProperty(None)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.delayed_init()
#mainthread
def delayed_init(self):
print(self.text)
Second, use a built-in method that is always executed after the first loop frame:
class RootWidget(Widget):
test = ObjectProperty(None)
def add_widget(self, widget, index=0):
super().add_widget(widget, index)
if widget.__class__.__name__ == 'Label':
widget.text = self.test.text
This is useful if you have a lot of same type widgets to bind.
Third, use a property's on_x callback:
class RootWidget(Widget):
test = ObjectProperty(None)
def on_test(self, *args):
print(self.test)
I have created a RelativeLayout subclass that positions its children in a grid by supplying them positions in the code (Python, not kv file). It works, but items are placed some 25 pixels to the upper-right from the position of layout itself, as shown by the canvas block. Python code for Layout subclass:
class RLMapWidget(RelativeLayout):
def __init__(self, map=None, **kwargs):
super(FloatLayout, self).__init__(**kwargs)
# Connecting to map, factories and other objects this class should know about
self.tile_factory = TileWidgetFactory()
self.map = map
# Initializing tile widgets for BG layer and adding them as children
for x in range(self.map.size[0]):
for y in range(self.map.size[1]):
tile_widget = self.tile_factory.create_tile_widget(self.map.get_item(layer='bg',
location=(x, y)))
# tile_widget.pos = (50*x, 50*y)
tile_widget.pos = self._get_screen_pos((x, y))
self.add_widget(tile_widget)
# Initializing widgets for actor layers
for x in range(self.map.size[0]):
for y in range(self.map.size[1]):
if self.map.has_item(layer='actors', location=(x, y)):
actor_widget = self.tile_factory.create_actor_widget(self.map.get_item(layer='actors',
displayed location=(x, y)))
actor_widget.pos=(50*x, 50*y)
self.add_widget(actor_widget)
# Map background canvas. Used solely to test positioning
with self.canvas.before:
Color(0, 0, 1, 1)
self.rect = Rectangle(size = self.size, pos=self.pos)
self.bind(pos=self.update_rect, size=self.update_rect)
# Initializing keyboard bindings and key lists
self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
self._keyboard.bind(on_key_down=self._on_key_down)
# The list of keys that will not be ignored by on_key_down
self.used_keys=['w', 'a', 's', 'd']
def redraw_actors(self):
for actor in self.map.actors:
actor.widget.pos = self._get_screen_pos(actor.location)
def _get_screen_pos(self, location):
"""
Return screen coordinates (in pixels) for a given location
:param location: int tuple
:return: int tuple
"""
return (location[0]*50, location[1]*50)
# Keyboard-related methods
def _on_key_down(self, keyboard, keycode, text, modifiers):
"""
Process keyboard event and make a turn, if necessary
:param keyboard:
:param keycode:
:param text:
:param modifiers:
:return:
"""
if keycode[1] in self.used_keys:
self.map.process_turn(keycode)
self.redraw_actors()
def _keyboard_closed(self):
self._keyboard.unbind(on_key_down=self._on_key_down)
self._keyboard = None
def update_rect(self, pos, size):
self.rect.pos = self.pos
self.rect.size = self.size
class CampApp(App):
def build(self):
root = FloatLayout()
map_factory = MapFactory()
map = map_factory.create_test_map()
map_widget = RLMapWidget(map=map,
size=(map.size[0]*50, map.size[1]*50),
size_hint=(None, None))
root.add_widget(map_widget)
return root
if __name__ == '__main__':
CampApp().run()
Factory class that makes tiles:
class TileWidgetFactory(object):
def __init__(self):
pass
def create_tile_widget(self, tile):
tile.widget = Image(source=tile.image_source,
size_hint=(None, None))
return tile.widget
def create_actor_widget(self, actor):
actor.widget = Image(source='Tmp_frame_black.png',
size_hint=(None, None))
return actor.widget
Okay, solved it myself. It turns out that if I supply the size to child widgets in factory, they are positioned properly. Although it solves the problem I have, I'd still be grateful if someone can explain where this quirk does come from.