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.
Related
Is it possible to generate an event in kivy so that an on_touch_up or on_touch_down function can be called without the user touching the screen?
First see the note in the documentation for Motion Event:
You never create the MotionEvent yourself: this is the role of the
providers.
Having said that, You can create and dispatch a minimal event like this:
class MyBoxLayout(BoxLayout):
def mydispatchAnEvent(self):
touch = MouseMotionEvent(None, 123, (123, 456)) # args are device, id, spos
touch.button = 'left'
touch.pos = (321, 654)
self.dispatch('on_touch_down', touch)
The class (in this case MyBoxLayout) must be an EventDispatcher. Calling mydispatchAnEvent() will dispatch a minimal event to all the children of MyBoxLayout.
With this code I can generate the touch_down event but for some reason it does not work with touch_up. if someone knows the answer it will be appreciated.
To switch a tab it needs both touch_down and touch_up.
enter code here
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.lang import Builder
from kivy.uix.floatlayout import FloatLayout
from kivy.input.providers.mouse import MouseMotionEvent
from kivymd.app import MDApp
from kivymd.uix.tab import MDTabsBase
KV = '''
BoxLayout:
orientation: "vertical"
MDTabs:
id: android_tabs
on_tab_switch: app.on_tab_switch(*args)
<Tab>:
MDLabel:
id: label
text: "Tab 0"
halign: "center"
'''
class Tab(FloatLayout, MDTabsBase):
'''Class implementing content for a tab.'''
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
def on_start(self):
for i in range(5):
self.root.ids.android_tabs.add_widget(Tab(title=f"Tab {i}"))
Window.bind(on_motion=self.on_motion)
Clock.schedule_once(self.dispatchTouchDown, 2)
Clock.schedule_once(self.dispatchTouchUp, 2.5)
def on_motion(self, obj, tevent, touch):
print (tevent, touch.sx*Window.width, touch.sy*Window.height)
def on_tab_switch(
self, instance_tabs, instance_tab, instance_tab_label, tab_text
):
instance_tab.ids.label.text = tab_text
def dispatchTouchDown(self, *args):
# create and dispatch a fake event
touch = MouseMotionEvent(None, 0, (0, 0)) # args are device, id, spos
touch.button = 'left'
touch.pos = (130, 570)
touch.x = touch.px = touch.ox = 130
touch.y = touch.py = touch.oy = 570
Window.dispatch('on_touch_down', touch)
#This works
def dispatchTouchUp(self, *args):
# create and dispatch a fake event
touch = MouseMotionEvent(None, 0, (0, 0)) # args are device, id, spos
touch.button = 'left'
touch.pos = (130, 570)
touch.x = touch.px = touch.ox = 130
touch.y = touch.py = touch.oy = 570
Window.dispatch('on_touch_up', touch)
#This does not work
Example().run()
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.
Following code opens window with label on top and label turns to textinput once you click on it.
But, once you start typing and insert first key (any key), text gets shortened, and you lose part of the text suddenly. For example: you click on label > textinput appears > you type '1' > text becomes 'Press here and then try 1'.
How to change below code to stop text disappearing?
import kivy
kivy.require('1.10.1')
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.textinput import TextInput
from kivy.uix.label import Label
from kivy.base import runTouchApp
from kivy.properties import BooleanProperty, ObjectProperty
#https://github.com/kivy/kivy/wiki/Editable-Label
class EditableLabel(Label):
edit = BooleanProperty(False)
textinput = ObjectProperty(None, allownone=True)
def on_touch_down(self, touch):
if self.collide_point(*touch.pos) and not self.edit:
self.edit = True
return super(EditableLabel, self).on_touch_down(touch)
def on_edit(self, instance, value):
if not value:
if self.textinput:
self.remove_widget(self.textinput)
return
self.textinput = t = TextInput(
text=self.text, size_hint=(None, None),
font_size=self.font_size, font_name=self.font_name,
pos=self.pos, size=self.size, multiline=False)
self.bind(pos=t.setter('pos'), size=t.setter('size'))
self.add_widget(self.textinput)
t.bind(on_text_validate=self.on_text_validate, focus=self.on_text_focus)
def on_text_validate(self, instance):
self.text = instance.text
self.edit = False
def on_text_focus(self, instance, focus):
if focus is False:
self.text = instance.text
self.edit = False
if __name__ == '__main__':
root = FloatLayout()
lbl = 'Press here and then try to edit (type a character), but text gets shortened suddenly.'
label = EditableLabel(text=lbl, size_hint_y=None, height=50, pos_hint={'top': 1})
root.add_widget(label)
runTouchApp(root)
According to the docs:
The selection is automatically updated when the cursor position changes. You can get the currently selected text from the TextInput.selection_text property.
and in your case when you click on the Label and appear in TextInput the cursor changes position so a text is selected. And when a text is selected and you write something it is replaced, that's why the text disappears.
The solution is to clean the selection:
from kivy.clock import Clock
class EditableLabel(Label):
[...]
def on_edit(self, instance, value):
if not value:
if self.textinput:
self.remove_widget(self.textinput)
return
self.textinput = t = TextInput(
text=self.text, size_hint=(None, None),
font_size=self.font_size, font_name=self.font_name,
pos=self.pos, size=self.size, multiline=False)
self.bind(pos=t.setter('pos'), size=t.setter('size'))
self.add_widget(self.textinput)
t.bind(on_text_validate=self.on_text_validate, focus=self.on_text_focus)
Clock.schedule_once(lambda dt: self.textinput.cancel_selection())
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]
I'm trying to make a game where you can move a character around screen and I had it so that if my character ran into a picture of a tree, the character would stop moving. After getting this to work, What I tried to do was change the code so instead of just using the tree variable, I wanted to iterate through a list of widgets, so that if my character runs into any of them, my character stops moving. What's strange is that it works when I have only one widget in the list. For example if I put list[0] or list[1] in my code, then my character will stop when encountering those widgets. But again, if I have more than one widget in the list and try to iterate through the list, it does not work, my character does not stop when encountering any of the widgets.
I'm guessing I did something wrong with the for loop. Ultimately I want it so that if my character runs into any of the images in the list, the character will stop moving.
Here is a snippet of my for loop, and below I have included the entire code (if that helps).
Snippet:
def on_keyboard_down(self, keyboard, keycode, text, modifiers):
if keycode[1] == 'left':
self.source = 'selectionscreen/left.zip'
self.anim_delay=.20
if self.x < (Window.width * .25):
bglayout.x += 4
else:
for i in listofwidgets:
if self.collide_widget(i):
self.x -=0
else:
self.x -=6
Full Code:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.image import Image
from kivy.core.window import Window
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.label import Label
from kivy.uix.behaviors import ButtonBehavior
from kivy.core.audio import SoundLoader
from kivy.uix.relativelayout import RelativeLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.screenmanager import FallOutTransition
from kivy.clock import Clock
from kivy.graphics import Color
gamelayout = FloatLayout(size=(300, 300))
bglayout = FloatLayout()
characterselectionlayout = GridLayout(cols=2)
class Game(Screen):
class Bg(Image):
def __init__(self, **kwargs):
super(Bg, self).__init__(**kwargs)
self.allow_stretch = True
self.size_hint = (None, None)
self.size = (1440, 1440)
class Npcs(Image):
def __init__(self, **kwargs):
super(Npcs, self).__init__(**kwargs)
self.allow_stretch=True
class MoveableImage(Image):
def __init__(self, **kwargs):
super(MoveableImage, self).__init__(**kwargs)
self._keyboard = Window.request_keyboard(None, self)
if not self._keyboard:
return
self._keyboard.bind(on_key_down=self.on_keyboard_down)
self._keyboard.bind(on_key_up=self.on_keyboard_up)
self.y = (Window.height/2.1)
self.app = App.get_running_app()
def on_keyboard_down(self, keyboard, keycode, text, modifiers):
if keycode[1] == 'left':
self.source = 'selectionscreen/left.zip'
self.anim_delay=.20
if self.x < (Window.width * .25):
bglayout.x += 4
else:
for i in listofwidgets:
if self.collide_widget(i):
self.x -=0
else:
self.x -=6
elif keycode[1] == 'right':
self.source ='selectionscreen/right.zip'
self.anim_delay=.20
if self.x > (Window.width * .70):
bglayout.x -= 4
else:
for i in listofwidgets:
if self.collide_widget(i):
self.x += 0
else:
self.x += 6
else:
return False
return True
class gameApp(App):
def build(self):
global sm
sm = ScreenManager()
game = Game(name='game')
sm.add_widget(game)
global listofobject
listofobject = []
hero = MoveableImage(source='selectionscreen/right1.png', size_hint=(None,None), allow_stretch = False, size=(40, 65))
self.tree = Npcs(source='selectionscreen/tree.zip', allow_stretch=False, size_hint=(None,None), pos_hint={'x':.20, 'y':.30}, size=(50, 50), pos=(300, 300))
self.testdude = Npcs(source='selectionscreen/testdude.png', allow_stretch=False, size_hint=(None,None), pos_hint={'x':.60, 'y':.70}, size=(100, 124), pos=(800, 900))
listofwidgets.append(self.tree)
listofwidgets.append(self.testdude)
self.background=Bg(source='selectionscreen/background12.png', pos_hint={'x':0, 'y':0})
bglayout.add_widget(self.background)
bglayout.add_widget(self.tree)
bglayout.add_widget(self.testdude)
gamelayout.add_widget(bglayout)
gamelayout.add_widget(hero)
game.add_widget(gamelayout)
return sm
if __name__ == '__main__':
gameApp().run()
The issue you're having is that your character is moved if he fails to collide with each of your objects. So if you have three objects and he doesn't hit any, he'll move at three times his normal speed. If he collides with one of them, he'll be slowed to twice his normal speed, but keep moving.
You need to change your code to test that he doesn't collide with anything before allowing him to move. The built in any function may help with this (call it on a generator expression):
if any(self.collide_widget(i) for i in listofwidgets):
self.x -=0
else:
self.x -=6
If you wanted to write an explicit loop, an equivalent one would be:
for i in listofwidgets:
if self.collide_widget(i):
self.x -= 0
break
else: # this block is run only if the loop ran to the end without breaking
self.x -= 6
Walk plus type() is handy here.
for widget in gameApp.walk(self, loopback=False):
if type(widget) == Npcs:
This will get you a particular widgets. You could think about rewriting your code somehow like this :
for widget in gameApp.walk(self, loopback=False):
if type(widget) == Npcs:
if not self.collide_widget(widget):
x-=6
break
But this looks wrong way round if this game will have a lots, lots of widgets.