I am using Kivy to design an application to draw an n-sided polygon over a live video stream to demarcate regions of interest. The problem I have is that Kivy provides coordinates w.r.t to the whole window and not just the image. What I would like is to have the location of the pixel (in x and y cords) clicked. I have looked at to_local() method but it didn't make much sense, neither did it produce desired results. Any help would be appreciated, below is the MRE.
from kivy.app import App
from kivy.uix.image import Image
from kivy.uix.boxlayout import BoxLayout
from kivy.graphics import Color, Ellipse, Line
from random import random
class ImageView(Image):
def on_touch_down(self, touch):
##
# This provides touch cords for the entire app and not just the location of the pixel clicked#
print("Touch Cords", touch.x, touch.y)
##
color = (random(), 1, 1)
with self.canvas:
Color(*color, mode='hsv')
d = 30.
Ellipse(pos=(touch.x - d / 2, touch.y - d / 2), size=(d, d))
touch.ud['line'] = Line(points=(touch.x, touch.y))
def on_touch_move(self, touch):
touch.ud['line'].points += [touch.x, touch.y]
class DMSApp(App):
def build(self):
imagewidget = ImageView(source="/home/red/Downloads/600.png")
imagewidget.size_hint = (1, .5)
imagewidget.pos_hint = {"top": 1}
layout = BoxLayout(size_hint=(1, 1))
layout.add_widget(imagewidget)
return layout
if __name__ == '__main__':
DMSApp().run()
You can calculate the coordinates of the touch relative to the lower left corner of the actual image that appears in the GUI. Those coordinates can then be scaled to the actual size of the source image to get a reasonable estimate of the actual pixel coordinates within the source. Here is a modified version of your in_touch_down() method that does that (only minimal testing performed):
def on_touch_down(self, touch):
if not self.collide_point(*touch.pos):
return super(ImageView, self).on_touch_down(touch)
lr_space = (self.width - self.norm_image_size[0]) / 2 # empty space in Image widget left and right of actual image
tb_space = (self.height - self.norm_image_size[1]) / 2 # empty space in Image widget above and below actual image
print('lr_space =', lr_space, ', tb_space =', tb_space)
print("Touch Cords", touch.x, touch.y)
print('Size of image within ImageView widget:', self.norm_image_size)
print('ImageView widget:, pos:', self.pos, ', size:', self.size)
print('image extents in x:', self.x + lr_space, self.right - lr_space)
print('image extents in y:', self.y + tb_space, self.top - tb_space)
pixel_x = touch.x - lr_space - self.x # x coordinate of touch measured from lower left of actual image
pixel_y = touch.y - tb_space - self.y # y coordinate of touch measured from lower left of actual image
if pixel_x < 0 or pixel_y < 0:
print('clicked outside of image\n')
return True
elif pixel_x > self.norm_image_size[0] or \
pixel_y > self.norm_image_size[1]:
print('clicked outside of image\n')
return True
else:
print('clicked inside image, coords:', pixel_x, pixel_y)
# scale coordinates to actual pixels of the Image source
print('actual pixel coords:',
pixel_x * self.texture_size[0] / self.norm_image_size[0],
pixel_y * self.texture_size[1] / self.norm_image_size[1], '\n')
color = (random(), 1, 1)
with self.canvas:
Color(*color, mode='hsv')
d = 30.
Ellipse(pos=(touch.x - d / 2, touch.y - d / 2), size=(d, d))
touch.ud['line'] = Line(points=(touch.x, touch.y))
return True
Am using custom shader on my game, and i must because of performance. Now i am at the point where i want to bind touch events to my particles/Ufo so that i can decide what to do when someone touches them, but i don't know how i can calculate their width and height. I am currently able to tell where a touch event happened but my collide_point function always return False because i don't have the correct Width and Height of my game particle's. collide_point function require particle's right and top and particle's right and top requires particle's width and height to work. In the documentation it is said that
the width and the height property is subject to layout logic
but am not using any Layout, am using Widget instead. How can i calculate my game particle's width and height. Below is the code
from __future__ import division
from collections import namedtuple
import json
import math
import random
from kivy import platform
from kivy.app import App
from kivy.base import EventLoop
from kivy.clock import Clock
from kivy.core.image import Image
from kivy.core.window import Window
from kivy.graphics import Mesh
from kivy.graphics.instructions import RenderContext
from kivy.uix.widget import Widget
from kivy.utils import get_color_from_hex
import base64
UVMapping = namedtuple('UVMapping', 'u0 v0 u1 v1 su sv')
GLSL = """
---vertex
$HEADER$
attribute vec2 vCenter;
attribute float vScale;
void main(void)
{
tex_coord0 = vTexCoords0;
mat4 move_mat = mat4
(1.0, 0.0, 0.0, vCenter.x,
0.0, 1.0, 0.0, vCenter.y,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0);
vec4 pos = vec4(vPosition.xy * vScale, 0.0, 1.0)
* move_mat;
gl_Position = projection_mat * modelview_mat * pos;
}
---fragment
$HEADER$
void main(void)
{
gl_FragColor = texture2D(texture0, tex_coord0);
}
"""
with open("game.glsl", "wb") as glslc:
glslc.write(GLSL)
def load_atlas():
atlas = json.loads('''{"game-0.png": {"Elien": [2, 26, 100, 100]}}''')
tex_name, mapping = atlas.popitem()
data = '''iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAACJklEQVR4nO3dy1ICQRAF0YLw/39ZVxMBGCjEMF23JvOsXPgounMaN61VQrtsH3x3TqFfLv9/ykdcq9z8RKv25Lro5yiUAcAZAJwBwBkAnAHAGQCcAcAZAJwBwBkAnAHAGQCcAcAZAJwBwBkAnAHAGQCcAcAZAJwBwBkAnAHAfXUPsNM79ydWXbYZZVoAey7MPH6tQdScAI64KbV9T3QI6QGsuCKHDiH5l8DVd1aRd2QTT4DOjcCdBmknQMpTmDLH4ZICSFv0tHkOkRJA6mKnzvUxCQGkL3L6fLskBKBG3QFMebqmzPm2zgCmLeq0eV/SfQKoWVcAU5+mqXM/5QkA1xHA9Kdo+vx3PAHgDADOAOBWB3CW98+zvA5PADoDgDMAOAOAMwA4A4AzADgDgDMAuNUBnOXCxVlehycAnQHAGQBcRwDT3z+nz3/HEwCuK4CpT9HUuZ/yBIDrDGDa0zRt3pd0nwBTFnXKnG/rDkDNEgJIf7rS59slIYCq3EVOnetjUgKoylvstHkOkRRAVc6ip8xxuMS/E7gtfsflC8zGb9JOgFurNwO3+VWZJ8CtFacBcuM36QFsjggBvfGbKQFsHjfNfxix07QAHrmpOyX/EqgFDADOAOAMAM4A4AwAzgDgDADOAOAMAM4A4AwAzgDgDADOAOAMAM4A4AwAzgDgDADOAOAMAM4A4AwA7lrl7YpE7okkSZIkSZIkSZIkSZIkSZIkSZL+9AMvSSThyPfOhQAAAABJRU5ErkJggg=='''
with open(tex_name, "wb") as co:
co.write(base64.b64decode(data))
tex = Image(tex_name).texture
tex_width, tex_height = tex.size
uvmap = {}
for name, val in mapping.items():
x0, y0, w, h = val
x1, y1 = x0 + w, y0 + h
uvmap[name] = UVMapping(
x0 / tex_width, 1 - y1 / tex_height,
x1 / tex_width, 1 - y0 / tex_height,
0.5 * w, 0.5 * h)
return tex, uvmap
class Particle:
x = 0
y = 0
size = 1
def __init__(self, parent, i):
self.parent = parent
self.vsize = parent.vsize
self.base_i = 4 * i * self.vsize
self.reset(created=True)
def update(self):
for i in range(self.base_i,
self.base_i + 4 * self.vsize,
self.vsize):
self.parent.vertices[i:i + 3] = (
self.x, self.y, self.size)
def reset(self, created=False):
raise NotImplementedError()
def advance(self, nap):
raise NotImplementedError()
class GameScreen(Widget):
indices = []
vertices = []
particles = []
def __init__(self, **kwargs):
Widget.__init__(self, **kwargs)
self.canvas = RenderContext(use_parent_projection=True)
self.canvas.shader.source = "game.glsl"
self.vfmt = (
(b'vCenter', 2, 'float'),
(b'vScale', 1, 'float'),
(b'vPosition', 2, 'float'),
(b'vTexCoords0', 2, 'float'),
)
self.vsize = sum(attr[1] for attr in self.vfmt)
self.texture, self.uvmap = load_atlas()
def on_touch_down(self, touch):
for w in self.particles:
if w.collide_point(*touch.pos):
w.reset() #Not Working
return super(GameScreen, self).on_touch_down(touch)
def on_touch_move(self, touch):
for w in self.particles:
if w.collide_point(*touch.pos):
w.reset() #Not Working
return super(GameScreen, self).on_touch_move(touch)
def make_particles(self, Ap, num):
count = len(self.particles)
uv = self.uvmap[Ap.tex_name]
for i in range(count, count + num):
j = 4 * i
self.indices.extend((
j, j + 1, j + 2, j + 2, j + 3, j))
self.vertices.extend((
0, 0, 1, -uv.su, -uv.sv, uv.u0, uv.v1,
0, 0, 1, uv.su, -uv.sv, uv.u1, uv.v1,
0, 0, 1, uv.su, uv.sv, uv.u1, uv.v0,
0, 0, 1, -uv.su, uv.sv, uv.u0, uv.v0,
))
p = Ap(self, i)
self.particles.append(p)
def update_glsl(self, nap):
for p in self.particles:
p.advance(nap)
p.update()
self.canvas.clear()
with self.canvas:
Mesh(fmt=self.vfmt, mode='triangles',
indices=self.indices, vertices=self.vertices,
texture=self.texture)
class Ufo(Particle):
plane = 2.0
tex_name = 'Elien'
texture_size = 129
right = top = 129
def reset(self, created=False):
self.plane = random.uniform(2.0, 2.8)
self.x = random.randint(15, self.parent.right-15)
self.y = self.parent.top+random.randint(100, 2500)
self.size = random.uniform(0.5, 1.0) #every particle must have a random size
self.top = self.size * self.texture_size
self.right = self.size * self.texture_size
def collide_point(self, x, y):
'''Check if a point (x, y) is inside the Ufo's axis aligned bounding box.'''
with open('TouchFeedback.txt', 'wb') as c:
c.write(str(x)+', '+str(y))
return self.x <= x <= self.right and self.y <= y <= self.top
def advance(self, nap):
self.y -= 100 * self.plane * nap
if self.y < 0:
self.reset()
class Game(GameScreen):
def initialize(self):
self.make_particles(Ufo, 20)
def update_glsl(self, nap):
GameScreen.update_glsl(self, nap)
class GameApp(App):
def build(self):
EventLoop.ensure_window()
return Game()
def on_start(self):
self.root.initialize()
Clock.schedule_interval(self.root.update_glsl, 60 ** -1)
if __name__ == '__main__':
Window.clearcolor = get_color_from_hex('111110')
GameApp().run()
I think you are not calculating/updating your top and right properties of your Ufo objects. Also, since your GL code considers (x,y) to be the center of your circle, but Kivy considers (x,y) to be the lower left corner of an object, you need to keep that in mind. In order to correctly calculate collide_point(), I have added left and bottom properties to your Ufo, and use those properties to calculate collisions. Here is an updated version of your code with those changes:
from __future__ import division
import kivy
from kivy.config import Config
from kivy.graphics.context_instructions import Color
from kivy.graphics.vertex_instructions import Rectangle
from kivy.lang import Builder
Config.set('modules', 'monitor', '')
from collections import namedtuple
import json
import math
import random
from kivy import platform
from kivy.app import App
from kivy.base import EventLoop
from kivy.clock import Clock
from kivy.core.image import Image
from kivy.core.window import Window
from kivy.event import EventDispatcher
from kivy.graphics import Mesh
from kivy.graphics.instructions import RenderContext
from kivy.properties import NumericProperty
from kivy.uix.widget import Widget
from kivy.utils import get_color_from_hex
import base64
UVMapping = namedtuple('UVMapping', 'u0 v0 u1 v1 su sv')
GLSL = """
---vertex
$HEADER$
attribute vec2 vCenter;
attribute float vScale;
void main(void)
{
tex_coord0 = vTexCoords0;
mat4 move_mat = mat4
(1.0, 0.0, 0.0, vCenter.x,
0.0, 1.0, 0.0, vCenter.y,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0);
vec4 pos = vec4(vPosition.xy * vScale, 0.0, 1.0)
* move_mat;
gl_Position = projection_mat * modelview_mat * pos;
}
---fragment
$HEADER$
void main(void)
{
gl_FragColor = texture2D(texture0, tex_coord0);
}
"""
with open("game.glsl", "wb") as glslc:
glslc.write(GLSL.encode())
def load_atlas():
atlas = json.loads('''{"game-0.png": {"Elien": [2, 26, 100, 100]}}''')
tex_name, mapping = atlas.popitem()
data = '''iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAACJklEQVR4nO3dy1ICQRAF0YLw/39ZVxMBGCjEMF23JvOsXPgounMaN61VQrtsH3x3TqFfLv9/ykdcq9z8RKv25Lro5yiUAcAZAJwBwBkAnAHAGQCcAcAZAJwBwBkAnAHAGQCcAcAZAJwBwBkAnAHAGQCcAcAZAJwBwBkAnAHAfXUPsNM79ydWXbYZZVoAey7MPH6tQdScAI64KbV9T3QI6QGsuCKHDiH5l8DVd1aRd2QTT4DOjcCdBmknQMpTmDLH4ZICSFv0tHkOkRJA6mKnzvUxCQGkL3L6fLskBKBG3QFMebqmzPm2zgCmLeq0eV/SfQKoWVcAU5+mqXM/5QkA1xHA9Kdo+vx3PAHgDADOAOBWB3CW98+zvA5PADoDgDMAOAOAMwA4A4AzADgDgDMAuNUBnOXCxVlehycAnQHAGQBcRwDT3z+nz3/HEwCuK4CpT9HUuZ/yBIDrDGDa0zRt3pd0nwBTFnXKnG/rDkDNEgJIf7rS59slIYCq3EVOnetjUgKoylvstHkOkRRAVc6ip8xxuMS/E7gtfsflC8zGb9JOgFurNwO3+VWZJ8CtFacBcuM36QFsjggBvfGbKQFsHjfNfxix07QAHrmpOyX/EqgFDADOAOAMAM4A4AwAzgDgDADOAOAMAM4A4AwAzgDgDADOAOAMAM4A4AwAzgDgDADOAOAMAM4A4AwA7lrl7YpE7okkSZIkSZIkSZIkSZIkSZIkSZL+9AMvSSThyPfOhQAAAABJRU5ErkJggg=='''
with open(tex_name, "wb") as co:
co.write(base64.b64decode(data))
tex = Image(tex_name).texture
tex_width, tex_height = tex.size
uvmap = {}
for name, val in mapping.items():
x0, y0, w, h = val
x1, y1 = x0 + w, y0 + h
uvmap[name] = UVMapping(
x0 / tex_width, 1 - y1 / tex_height,
x1 / tex_width, 1 - y0 / tex_height,
0.5 * w, 0.5 * h)
return tex, uvmap
class Particle(EventDispatcher):
# x = 0
# y = 0
x = NumericProperty(0)
y = NumericProperty(0)
size = 1
def __init__(self, parent, i):
super(Particle, self).__init__()
self.parent = parent
self.vsize = parent.vsize
self.base_i = 4 * i * self.vsize
self.reset(created=True)
def update(self):
for i in range(self.base_i,
self.base_i + 4 * self.vsize,
self.vsize):
self.parent.vertices[i:i + 3] = (
self.x, self.y, self.size)
def reset(self, created=False):
raise NotImplementedError()
def advance(self, nap):
raise NotImplementedError()
class GameScreen(Widget):
indices = []
vertices = []
particles = []
def __init__(self, **kwargs):
Widget.__init__(self, **kwargs)
self.canvas = RenderContext(use_parent_projection=True)
self.canvas.shader.source = "game.glsl"
self.vfmt = (
(b'vCenter', 2, 'float'),
(b'vScale', 1, 'float'),
(b'vPosition', 2, 'float'),
(b'vTexCoords0', 2, 'float'),
)
self.vsize = sum(attr[1] for attr in self.vfmt)
self.texture, self.uvmap = load_atlas()
def on_touch_down(self, touch):
for w in self.particles:
if w.collide_point(*touch.pos):
w.reset() #Not Working
return super(GameScreen, self).on_touch_down(touch)
def on_touch_move(self, touch):
for w in self.particles:
if w.collide_point(*touch.pos):
w.reset() #Not Working
return super(GameScreen, self).on_touch_move(touch)
def make_particles(self, Ap, num):
count = len(self.particles)
uv = self.uvmap[Ap.tex_name]
for i in range(count, count + num):
j = 4 * i
self.indices.extend((
j, j + 1, j + 2, j + 2, j + 3, j))
self.vertices.extend((
0, 0, 1, -uv.su, -uv.sv, uv.u0, uv.v1,
0, 0, 1, uv.su, -uv.sv, uv.u1, uv.v1,
0, 0, 1, uv.su, uv.sv, uv.u1, uv.v0,
0, 0, 1, -uv.su, uv.sv, uv.u0, uv.v0,
))
p = Ap(self, i)
self.particles.append(p)
def update_glsl(self, nap):
for p in self.particles:
p.advance(nap)
p.update()
self.canvas.clear()
self.canvas.before.clear() # temporary
with self.canvas.before: # temporary code block
for p in self.particles:
Rectangle(pos=(p.left, p.bottom), size=(p.size*p.texture_size, p.size*p.texture_size))
with self.canvas:
Mesh(fmt=self.vfmt, mode='triangles',
indices=self.indices, vertices=self.vertices,
texture=self.texture)
class Ufo(Particle):
plane = 2.0
tex_name = 'Elien'
texture_size = 129
right = NumericProperty(129)
top = NumericProperty(129)
left = NumericProperty(0)
bottom = NumericProperty(0)
def reset(self, created=False):
self.plane = random.uniform(2.0, 2.8)
self.size = random.uniform(0.5, 1.0) #every particle must have a random size
self.x = random.randint(15, self.parent.right-15)
self.y = self.parent.top+random.randint(100, 2500)
def collide_point(self, x, y):
'''Check if a point (x, y) is inside the Ufo's axis aligned bounding box.'''
return self.left <= x <= self.right and self.bottom <= y <= self.top
def advance(self, nap):
self.y -= 100 * self.plane * nap
if self.y < 0:
self.reset()
def on_x(self, instance, new_x):
self.right = new_x + self.size * self.texture_size / 2.0
self.left = new_x - self.size * self.texture_size / 2.0
def on_y(self, instance, new_y):
self.top = new_y + self.size * self.texture_size / 2.0
self.bottom = new_y - self.size * self.texture_size / 2.0
class Game(GameScreen):
def initialize(self):
self.make_particles(Ufo, 20)
def update_glsl(self, nap):
GameScreen.update_glsl(self, nap)
class GameApp(App):
def build(self):
EventLoop.ensure_window()
return Game()
def on_start(self):
self.root.initialize()
Clock.schedule_interval(self.root.update_glsl, 60 ** -1)
if __name__ == '__main__':
Window.clearcolor = get_color_from_hex('111110')
GameApp().run()
I have also adding drawing the Ufo bounding box using canvas.before. This is just to visualize the clickable area for each Ufo, and can be easily removed.
Try using collide_widget()
Snippets
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
for w in self.particles:
if self.collide_widget(w):
w.reset()
return True
return super(GameScreen, self).on_touch_down(touch)
Widget class ยป collide_widget
collide_widget(wid)
Check if another widget collides with this widget. This function performs an axis-aligned bounding box intersection test by default.
Parameters:
wid: Widget class
Widget to test collision with.
Returns:
bool. True if the other widget collides with this widget, False otherwise.
I have a Point class and Rectangle class set up, here is the code for that:
import math
class Point:
"""A point in two-dimensional space."""
def __init__(self, x: float = 0.0, y: float = 0.0)->None:
self.x = x
self.y = y
def moveIt(self, dx: float, dy: float)-> None:
self.x = self.x + dx
self.y = self.y + dy
def distance(self, otherPoint: float):
if isinstance(otherPoint, Point):
x1 = self.x
y1 = self.y
x2 = otherPoint.x
y2 = otherPoint.y
return ( (x1 - x2)**2 + (y1 - y2)**2 )**0.5
class Rectangle:
def __init__(self, topLeft = Point(0,0), bottomRight = Point(1,1)):
self.topLeft = topLeft
self.bottomRight = bottomRight
The two points are the top left and the bottom right for the rectangle. How could I find the area and perimeter of this rectangle from two points? Would appreciate any and all help!
We can access the x and y values of each point and calculate the height and width, from there we can create methods that calculate area and perimeter
class Rectangle():
def __init__(self, topLeft = Point(0,0), bottomRight = Point(1,1)):
self.topLeft = topLeft
self.bottomRight = bottomRight
self.height = topLeft.y - bottomRight.y
self.width = bottomRight.x - topLeft.x
self.perimeter = (self.height + self.width) * 2
self.area = self.height * self.width
rect = Rectangle(Point(3,10),Point(4,8))
print(rect.height)
print(rect.width)
print(rect.perimeter)
print(rect.area)
chrx#chrx:~/python/stackoverflow/9.24$ python3.7 rect.py
2
1
6
2
Or using methods
class Rectangle():
def __init__(self, topLeft = Point(0,0), bottomRight = Point(1,1)):
self.topLeft = topLeft
self.bottomRight = bottomRight
self.height = topLeft.y - bottomRight.y
self.width = bottomRight.x - topLeft.x
def make_perimeter(self):
self.perimeter = (self.height + self.width) * 2
return self.perimeter
def make_area(self):
self.area = self.height * self.width
return self.area
rect = Rectangle(Point(3,10),Point(4,8))
print(rect.height)
print(rect.width)
print(rect.make_perimeter())
print(rect.make_area())
I need to change orientation to vertical, but it does not work the same way as BoxLayout. There is also no information about this in the Kivy official documentation. In addition, is there any way to change the page by swiping from any place on the screen and not only the border?
Python:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen, FadeTransition
class MainScreen(Screen):
pass
class ScreenManagement(ScreenManager):
pass
presentation = Builder.load_file("final.kv")
class MainApp(App):
def build(self):
return presentation
if __name__ == "__main__":
MainApp().run()
Kivy:
ScreenManagement:
MainScreen:
<MainScreen>:
canvas:
Rectangle:
source: "nakedman.jpg"
pos: self.pos
size: self.size
name: "main"
PageLayout:
orientation: "vertical"
BoxLayout:
Button:
text: "Button1"
Button:
text: "Button2"
BoxLayout:
Button:
text: "Button3"
Button:
text: "Button4"
PageLayout does not have the orientation attribute, so you have to build everything, in this case use the source code of PageLayout to obtain this new code:
__all__ = ('PageLayoutVertical', )
from kivy.uix.layout import Layout
from kivy.properties import NumericProperty, DictProperty
from kivy.animation import Animation
class PageLayoutVertical(Layout):
'''PageLayout class. See module documentation for more information.
'''
page = NumericProperty(0)
'''The currently displayed page.
:data:`page` is a :class:`~kivy.properties.NumericProperty` and defaults
to 0.
'''
border = NumericProperty('50dp')
'''The width of the border around the current page used to display
the previous/next page swipe areas when needed.
:data:`border` is a :class:`~kivy.properties.NumericProperty` and
defaults to 50dp.
'''
swipe_threshold = NumericProperty(.5)
'''The thresold used to trigger swipes as percentage of the widget
size.
:data:`swipe_threshold` is a :class:`~kivy.properties.NumericProperty`
and defaults to .5.
'''
anim_kwargs = DictProperty({'d': .5, 't': 'in_quad'})
'''The animation kwargs used to construct the animation
:data:`anim_kwargs` is a :class:`~kivy.properties.DictProperty`
and defaults to {'d': .5, 't': 'in_quad'}.
.. versionadded:: 1.11.0
'''
def __init__(self, **kwargs):
super(PageLayoutVertical, self).__init__(**kwargs)
trigger = self._trigger_layout
fbind = self.fbind
fbind('border', trigger)
fbind('page', trigger)
fbind('parent', trigger)
fbind('children', trigger)
fbind('size', trigger)
fbind('pos', trigger)
def do_layout(self, *largs):
l_children = len(self.children) - 1
w = self.width
x_parent, y_parent = self.pos
p = self.page
border = self.border
half_border = border / 2.
top = self.top
height = self.height - border
for i, c in enumerate(reversed(self.children)):
if i < p:
y = y_parent
elif i == p:
if not p: # it's first page
y = y_parent
elif p != l_children: # not first, but there are post pages
y = y_parent + half_border
else: # not first and there are no post pages
y = y_parent + border
elif i == p + 1:
if not p: # second page - no left margin
y = top - border
else: # there's already a left margin
y = top - half_border
else:
y = top
c.width = w
c.height = height
Animation(
x=x_parent,
y=y,
**self.anim_kwargs).start(c)
def on_touch_down(self, touch):
if (
self.disabled or
not self.collide_point(*touch.pos) or
not self.children
):
return
page = self.children[-self.page - 1]
if self.y <= touch.y < page.y:
touch.ud['page'] = 'previous'
touch.grab(self)
return True
elif page.top <= touch.y < self.top:
touch.ud['page'] = 'next'
touch.grab(self)
return True
return page.on_touch_down(touch)
def on_touch_move(self, touch):
if touch.grab_current != self:
return
p = self.page
border = self.border
half_border = border / 2.
page = self.children[-p - 1]
if touch.ud['page'] == 'previous':
# move next page upto right edge
if p < len(self.children) - 1:
self.children[-p - 2].y = min(
self.top - self.border * (1 - (touch.sy - touch.osy)),
self.top)
# move current page until edge hits the right border
if p >= 1:
b_right = half_border if p > 1 else border
b_left = half_border if p < len(self.children) - 1 else border
self.children[-p - 1].y = max(min(
self.y + b_left + (touch.y - touch.oy),
self.top - b_right),
self.y + b_left)
# move previous page left edge upto left border
if p > 1:
self.children[-p].y = min(
self.y + half_border * (touch.sy - touch.osy),
self.y + half_border)
elif touch.ud['page'] == 'next':
# move current page upto left edge
if p >= 1:
self.children[-p - 1].y = max(
self.y + half_border * (1 - (touch.osy - touch.sy)),
self.y)
# move next page until its edge hit the left border
if p < len(self.children) - 1:
b_right = half_border if p >= 1 else border
b_left = half_border if p < len(self.children) - 2 else border
self.children[-p - 2].y = min(max(
self.top - b_right + (touch.y - touch.oy),
self.y + b_left),
self.top - b_right)
# move second next page upto right border
if p < len(self.children) - 2:
self.children[-p - 3].y = max(
self.top + half_border * (touch.sy - touch.osy),
self.top - half_border)
return page.on_touch_move(touch)
def on_touch_up(self, touch):
if touch.grab_current == self:
if (
touch.ud['page'] == 'previous' and
abs(touch.y - touch.oy) / self.height > self.swipe_threshold
):
self.page -= 1
elif (
touch.ud['page'] == 'next' and
abs(touch.y - touch.oy) / self.height > self.swipe_threshold
):
self.page += 1
else:
self._trigger_layout()
touch.ungrab(self)
if len(self.children) > 1:
return self.children[-self.page + 1].on_touch_up(touch)
if __name__ == '__main__':
from kivy.base import runTouchApp
from kivy.uix.button import Button
pl = PageLayoutVertical()
for i in range(1, 10):
b = Button(text='page%s' % i)
pl.add_widget(b)
runTouchApp(pl)
I've got two three nested widget which handle the on_touch_down event, clicking on the first parent, the event is sensed to the second and then the third. With this click the third widget should draw on it's canvas but this it's not happening.
I understand the dynamic of the canvas object and it's groups. So I don't really understand why this is happening
class Spline_guide(DragBehavior, Button):
def __init__(self, **kw):
super(Spline_guide, self).__init__(**kw)
self.bind(pos = self.update_spline , size = self.update_spline)
def update_spline(self, *l):
self.pos = self.parent.pos
return self
def show_spline(self,*l):
print 'show spline'
with self.canvas:
Color(0,1,0)
######## THIS ISTRUCTION DOESN'T WORK
Ellipse(pos = (100,100), size = (100,100))
return self
def hide_spline(self, *l):
print 'hide spline'
self.canvas.clear()
return self
class Editable_point(DragBehavior, Widget):
def __init__(self, name,x,y, **kw):
super(Editable_point, self).__init__(**kw)
self.drag_rectangle = (0,0 ,800,300)
self.drag_timeout = 10000000
self.drag_distance = 0
self.name = name
self.pos = (x - DOT_DIMENSION / 2,y - DOT_DIMENSION / 2)
self.size = (DOT_DIMENSION, DOT_DIMENSION)
self.spline = Spline_guide()
self.add_widget(self.spline)
self.SHOW_SPLINE = False
self.bind(pos = self.check_pos)
def check_pos(self, *l):
self.area = self.parent.parent
self.x = self.x if self.x > self.area.x else self.area.x
self.y = self.y if self.y > self.area.y else self.area.y
return self
def draw_point(self):
self.area = self.parent.parent
self.drag_rectangle = (self.area.x, self.area.y, self.area.width, self.area.height)
self.canvas.clear()
with self.canvas:
Color(1,0,0)
Ellipse(pos=self.pos, size=self.size)
return self
def on_enter(self, pop):
def wrap(l):
if l.text in ['start','stop']:
print 'you can not use start or stop as name'
pop.dismiss()
return wrap
if self.name in ['start','stop']:
pop.dismiss()
print 'you can not edit the name of start or stop point'
return wrap
self.name = l.text
pop.dismiss()
return wrap
def show_info(self, *l):
graph = self.parent.parent.graph
X_OFFSET = graph._plot_area.x + self.parent.x - DOT_DIMENSION / 2
Y_OFFSET = graph._plot_area.y + self.parent.y - DOT_DIMENSION / 2
_x = self.x - X_OFFSET
_y = self.y - Y_OFFSET
x, y = normalize(graph.xmin,graph.ymin,graph.xmax,graph.ymax,graph._plot_area.width,graph._plot_area.height, _x, _y)
point_name = TextInput(text=self.name, multiline = False)
point_info = Popup(title ="X: {} Y: {}".format(x,y), content = point_name, size_hint=(None,None), size=(200,100))
point_name.bind(on_text_validate=self.on_enter(point_info))
point_info.open()
return self
def on_touch_down(self, *l):
if self.SHOW_SPLINE:
self.SHOW_SPLINE = False
self.spline.hide_spline()
else:
self.SHOW_SPLINE = True
self.spline.show_spline()
return self
class Editable_line(Widget):
def __init__(self, **kw):
super(Editable_line, self).__init__(**kw)
self._old_ = [100,100]
self.old_pos = [0,0]
self.bind(pos=self.update_line, size=self.update_line)
def replace_start_stop(self, new_elem):
elem = filter(lambda x: x.name == new_elem.name, [ i for i in self.children if hasattr(i,'name')])
if elem: self.remove_widget(elem[0])
self.add_widget(new_elem)
return self
def update_points(self):
r_x = float(Window.size[0]) / float(self._old_[0])
r_y = float(Window.size[1]) / float(self._old_[1])
for p in [ i for i in self.children if hasattr(i,'name')]:
new_x = p.x * r_x
new_y = p.y * r_y
p.x = new_x
p.y = new_y
p.size = (DOT_DIMENSION, DOT_DIMENSION)
return self
def update_line(self, *a):
self.pos = self.parent.pos
self.size = self.parent.size
self.parent.graph.pos = self.pos
self.parent.graph.size = self.size
self.parent.graph._redraw_size()
#Coordinate per start e stop personalizzare sul graph
y = self.parent.graph._plot_area.y + self.y
x = self.parent.graph._plot_area.x + self.x
h = self.parent.graph._plot_area.height
w = self.parent.graph._plot_area.width
self.replace_start_stop(Editable_point('start', x, y + h / 2))
self.replace_start_stop(Editable_point('stop', x + w, y + h / 2))
self.update_points()
self._old_ = Window.size
return self.draw_line()
def sort_points(self):
self.children = sorted(self.children , key=attrgetter('x'))
return self
def control_presence(self,coordinates):
x = int(coordinates.x)
y = int(coordinates.y)
x_range = range(x-DOT_DIMENSION, x+DOT_DIMENSION)
y_range = range(y-DOT_DIMENSION, y+DOT_DIMENSION)
for p in [ i for i in self.children if hasattr(i,'name')]:
if int(p.x) in x_range and int(p.y) in y_range: return p
return False
def on_touch_down(self,coordinates):
#add points
p = self.control_presence(coordinates)
if p:
if not coordinates.is_double_tap:
return p.on_touch_down(coordinates)
return p.show_info(coordinates)
x = int(coordinates.x)
y = int(coordinates.y)
p = Editable_point('new point', x, y)
p.size = (DOT_DIMENSION, DOT_DIMENSION)
self.add_widget(p)
return self.draw_line()
def remove_point(self,coordinates):
p = self.control_presence(coordinates)
if p:
if p.name in ['start','stop']: print 'you can\'t delete start or stop point' else: self.remove_widget(p)
return self.draw_line()
def draw_line(self):
self.sort_points()
self.canvas.before.clear()
_l = list()
for p in [ i for i in self.children if hasattr(i,'name')]:
_l.append(p.x + DOT_DIMENSION/2)
_l.append(p.y + DOT_DIMENSION/2)
p.draw_point()
with self.canvas.before:
Color(0,0.8,1)
Line(points=_l, witdth = LINE_WIDTH)
return self
def on_touch_move(self, coordinates):
p = self.control_presence(coordinates)
if p:
if p.name in ['start','stop']:
return self.parent.on_touch_up(coordinates)
p.on_touch_move(coordinates)
self.draw_line()
return self
This is a piece of my code and show how the classes are linked. The Spline_guide doesn't draw anything. Ideas?
Widgets which are added to another Widget are drawn in the latter Widget's canvas. When you call self.canvas.clear() (i.e. in Editable_point) you are removing all the child canvases.
You could use canvas.before or canvas.after for your drawing instead, or you can save the instructions and modify them later:
def __init__(self, **kwargs):
...
with self.canvas:
self.draw_color = Color(0, 1, 0, 1)
self.draw_ellipse = Ellipse(pos=self.pos, size=self.size)
def draw_point(self):
...
self.draw_ellipse.pos = self.pos
self.draw_ellipse.size = self.size
Then you never need to clear the canvas at all. This is the preferred solution as it offers the best performance.