'''
Inspector
=========
.. versionadded:: 1.0.9
.. warning::
This module is highly experimental, use it with care.
The Inspector is a tool for finding a widget in the widget tree by clicking or
tapping on it.
Some keyboard shortcuts are activated:
* "Ctrl + e": activate / deactivate the inspector view
* "Escape": cancel widget lookup first, then hide the inspector view
Available inspector interactions:
* tap once on a widget to select it without leaving inspect mode
* double tap on a widget to select and leave inspect mode (then you can
manipulate the widget again)
Some properties can be edited live. However, due to the delayed usage of
some properties, it might crash if you don't handle all the cases.
Usage
-----
For normal module usage, please see the :mod:`~kivy.modules` documentation.
The Inspector, however, can also be imported and used just like a normal
python module. This has the added advantage of being able to activate and
deactivate the module programmatically::
from kivy.core.window import Window
from kivy.app import App
from kivy.uix.button import Button
from kivy.modules import inspector
class Demo(App):
def build(self):
button = Button(text="Test")
inspector.create_inspector(Window, button)
return button
Demo().run()
To remove the Inspector, you can do the following::
inspector.stop(Window, button)
'''
__all__ = ('start', 'stop', 'create_inspector')
import kivy
kivy.require('1.0.9')
import weakref
from functools import partial
from itertools import chain
from kivy.animation import Animation
from kivy.logger import Logger
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.togglebutton import ToggleButton
from kivy.uix.textinput import TextInput
from kivy.uix.image import Image
from kivy.uix.treeview import TreeViewNode, TreeView
from kivy.uix.gridlayout import GridLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.modalview import ModalView
from kivy.graphics import Color, Rectangle, PushMatrix, PopMatrix
from kivy.graphics.context_instructions import Transform
from kivy.graphics.transformation import Matrix
from kivy.properties import ObjectProperty, BooleanProperty, ListProperty, \
NumericProperty, StringProperty, OptionProperty, \
ReferenceListProperty, AliasProperty, VariableListProperty
from kivy.graphics.texture import Texture
from kivy.clock import Clock
from kivy.lang import Builder
Builder.load_string('''
<Inspector>:
layout: layout
widgettree: widgettree
treeview: treeview
content: content
BoxLayout:
orientation: 'vertical'
id: layout
size_hint_y: None
height: 250
padding: 5
spacing: 5
top: 0
canvas:
Color:
rgb: .4, .4, .4
Rectangle:
pos: self.x, self.top
size: self.width, 1
Color:
rgba: .185, .18, .18, .95
Rectangle:
pos: self.pos
size: self.size
# Top Bar
BoxLayout:
size_hint_y: None
height: 50
spacing: 5
Button:
text: 'Move to Top'
on_release: root.toggle_position(args[0])
size_hint_x: None
width: 120
ToggleButton:
text: 'Inspect'
on_state: root.inspect_enabled = args[1] == 'down'
size_hint_x: None
state: 'down' if root.inspect_enabled else 'normal'
width: 80
Button:
text: 'Parent'
on_release:
root.highlight_widget(root.widget.parent) if root.widget \
else None
size_hint_x: None
width: 80
Button:
text: '%r' % root.widget
on_release: root.show_widget_info()
Button:
text: 'X'
size_hint_x: None
width: 50
on_release: root.activated = False
# Bottom Bar
BoxLayout:
ScrollView:
scroll_type: ['bars', 'content']
bar_width: 10
size_hint_x: 0.0001
WidgetTree:
id: widgettree
hide_root: True
size_hint: None, None
height: self.minimum_height
width: max(self.parent.width, self.minimum_width)
selected_widget: root.widget
on_select_widget: root.highlight_widget(args[1])
Splitter:
sizeable_from: 'left'
min_size: self.parent.width / 2
max_size: self.parent.width
BoxLayout:
ScrollView:
scroll_type: ['bars', 'content']
bar_width: 10
TreeView:
id: treeview
size_hint_y: None
hide_root: True
height: self.minimum_height
Splitter:
sizeable_from: 'left'
keep_within_parent: True
rescale_with_parent: True
max_size: self.parent.width / 2
min_size: 0
ScrollView:
id: content
<TreeViewProperty>:
height: max(lkey.texture_size[1], ltext.texture_size[1])
Label:
id: lkey
text: root.key
text_size: (self.width, None)
width: 150
size_hint_x: None
Label:
id: ltext
text: [repr(getattr(root.widget, root.key, '')), root.refresh][0]\
if root.widget else ''
text_size: (self.width, None)
<-TreeViewWidget>:
height: self.texture_size[1] + sp(4)
size_hint_x: None
width: self.texture_size[0] + sp(4)
canvas.before:
Color:
rgba: self.color_selected if self.is_selected else (0, 0, 0, 0)
Rectangle:
pos: self.pos
size: self.size
Color:
rgba: 1, 1, 1, int(not self.is_leaf)
Rectangle:
source:
('atlas://data/images/defaulttheme/tree_%s' %
('opened' if self.is_open else 'closed'))
size: 16, 16
pos: self.x - 20, self.center_y - 8
canvas:
Color:
rgba:
(self.disabled_color if self.disabled else
(self.color if not self.markup else (1, 1, 1, 1)))
Rectangle:
texture: self.texture
size: self.texture_size
pos:
(int(self.center_x - self.texture_size[0] / 2.),
int(self.center_y - self.texture_size[1] / 2.))
''')
class TreeViewProperty(BoxLayout, TreeViewNode):
widget_ref = ObjectProperty(None, allownone=True)
def _get_widget(self):
wr = self.widget_ref
if wr is None:
return
wr = wr()
if wr is None:
self.widget_ref = None
return
return wr
widget = AliasProperty(_get_widget, None, bind=('widget_ref', ))
key = ObjectProperty(None, allownone=True)
inspector = ObjectProperty(None)
refresh = BooleanProperty(False)
class TreeViewWidget(Label, TreeViewNode):
widget = ObjectProperty(None)
class WidgetTree(TreeView):
selected_widget = ObjectProperty(None, allownone=True)
__events__ = ('on_select_widget',)
def __init__(self, **kwargs):
super(WidgetTree, self).__init__(**kwargs)
self.update_scroll = Clock.create_trigger(self._update_scroll)
def find_node_by_widget(self, widget):
for node in self.iterate_all_nodes():
if not node.parent_node:
continue
try:
if node.widget == widget:
return node
except ReferenceError:
pass
return
def update_selected_widget(self, widget):
if widget:
node = self.find_node_by_widget(widget)
if node:
self.select_node(node, False)
while node and isinstance(node, TreeViewWidget):
if not node.is_open:
self.toggle_node(node)
node = node.parent_node
def on_selected_widget(self, inst, widget):
if widget:
self.update_selected_widget(widget)
self.update_scroll()
def select_node(self, node, select_widget=True):
super(WidgetTree, self).select_node(node)
if select_widget:
try:
self.dispatch('on_select_widget', node.widget.__self__)
except ReferenceError:
pass
def on_select_widget(self, widget):
pass
def _update_scroll(self, *args):
node = self._selected_node
if not node:
return
self.parent.scroll_to(node)
class Inspector(FloatLayout):
widget = ObjectProperty(None, allownone=True)
layout = ObjectProperty(None)
widgettree = ObjectProperty(None)
treeview = ObjectProperty(None)
inspect_enabled = BooleanProperty(False)
activated = BooleanProperty(False)
widget_info = BooleanProperty(False)
content = ObjectProperty(None)
at_bottom = BooleanProperty(True)
_update_widget_tree_ev = None
def __init__(self, **kwargs):
self.win = kwargs.pop('win', None)
super(Inspector, self).__init__(**kwargs)
self.avoid_bring_to_top = False
with self.canvas.before:
self.gcolor = Color(1, 0, 0, .25)
PushMatrix()
self.gtransform = Transform(Matrix())
self.grect = Rectangle(size=(0, 0))
PopMatrix()
Clock.schedule_interval(self.update_widget_graphics, 0)
def on_touch_down(self, touch):
ret = super(Inspector, self).on_touch_down(touch)
if (('button' not in touch.profile or touch.button == 'left') and
not ret and self.inspect_enabled):
self.highlight_at(*touch.pos)
if touch.is_double_tap:
self.inspect_enabled = False
self.show_widget_info()
ret = True
return ret
def on_touch_move(self, touch):
ret = super(Inspector, self).on_touch_move(touch)
if not ret and self.inspect_enabled:
self.highlight_at(*touch.pos)
ret = True
return ret
def on_touch_up(self, touch):
ret = super(Inspector, self).on_touch_up(touch)
if not ret and self.inspect_enabled:
ret = True
return ret
def on_window_children(self, win, children):
if self.avoid_bring_to_top or not self.activated:
return
self.avoid_bring_to_top = True
win.remove_widget(self)
win.add_widget(self)
self.avoid_bring_to_top = False
def highlight_at(self, x, y):
widget = None
# reverse the loop - look at children on top first and
# modalviews before others
win_children = self.win.children
children = chain(
(c for c in win_children if isinstance(c, ModalView)),
(c for c in reversed(win_children) if not isinstance(c, ModalView))
)
for child in children:
if child is self:
continue
widget = self.pick(child, x, y)
if widget:
break
self.highlight_widget(widget)
def highlight_widget(self, widget, info=True, *largs):
# no widget to highlight, reduce rectangle to 0, 0
self.widget = widget
if not widget:
self.grect.size = 0, 0
if self.widget_info and info:
self.show_widget_info()
def update_widget_graphics(self, *l):
if not self.activated:
return
if self.widget is None:
self.grect.size = 0, 0
return
self.grect.size = self.widget.size
matrix = self.widget.get_window_matrix()
if self.gtransform.matrix.get() != matrix.get():
self.gtransform.matrix = matrix
def toggle_position(self, button):
to_bottom = button.text == 'Move to Bottom'
if to_bottom:
button.text = 'Move to Top'
if self.widget_info:
Animation(top=250, t='out_quad', d=.3).start(self.layout)
else:
Animation(top=60, t='out_quad', d=.3).start(self.layout)
bottom_bar = self.layout.children[1]
self.layout.remove_widget(bottom_bar)
self.layout.add_widget(bottom_bar)
else:
button.text = 'Move to Bottom'
if self.widget_info:
Animation(top=self.height, t='out_quad', d=.3).start(
self.layout)
else:
Animation(y=self.height - 60, t='out_quad', d=.3).start(
self.layout)
bottom_bar = self.layout.children[1]
self.layout.remove_widget(bottom_bar)
self.layout.add_widget(bottom_bar)
self.at_bottom = to_bottom
def pick(self, widget, x, y):
ret = None
# try to filter widgets that are not visible (invalid inspect target)
if (hasattr(widget, 'visible') and not widget.visible):
return ret
if widget.collide_point(x, y):
ret = widget
x2, y2 = widget.to_local(x, y)
# reverse the loop - look at children on top first
for child in reversed(widget.children):
ret = self.pick(child, x2, y2) or ret
return ret
def on_activated(self, instance, activated):
if not activated:
self.grect.size = 0, 0
if self.at_bottom:
anim = Animation(top=0, t='out_quad', d=.3)
else:
anim = Animation(y=self.height, t='out_quad', d=.3)
anim.bind(on_complete=self.animation_close)
anim.start(self.layout)
self.widget = None
self.widget_info = False
else:
self.win.add_widget(self)
Logger.info('Inspector: inspector activated')
if self.at_bottom:
Animation(top=60, t='out_quad', d=.3).start(self.layout)
else:
Animation(y=self.height - 60, t='out_quad', d=.3).start(
self.layout)
ev = self._update_widget_tree_ev
if ev is None:
ev = self._update_widget_tree_ev = Clock.schedule_interval(
self.update_widget_tree, 1)
else:
ev()
self.update_widget_tree()
def animation_close(self, instance, value):
if not self.activated:
self.inspect_enabled = False
self.win.remove_widget(self)
self.content.clear_widgets()
treeview = self.treeview
for node in list(treeview.iterate_all_nodes()):
node.widget_ref = None
treeview.remove_node(node)
self._window_node = None
if self._update_widget_tree_ev is not None:
self._update_widget_tree_ev.cancel()
widgettree = self.widgettree
for node in list(widgettree.iterate_all_nodes()):
widgettree.remove_node(node)
Logger.info('Inspector: inspector deactivated')
def show_widget_info(self):
self.content.clear_widgets()
widget = self.widget
treeview = self.treeview
for node in list(treeview.iterate_all_nodes())[:]:
node.widget_ref = None
treeview.remove_node(node)
if not widget:
if self.at_bottom:
Animation(top=60, t='out_quad', d=.3).start(self.layout)
else:
Animation(y=self.height - 60, t='out_quad', d=.3).start(
self.layout)
self.widget_info = False
return
self.widget_info = True
if self.at_bottom:
Animation(top=250, t='out_quad', d=.3).start(self.layout)
else:
Animation(top=self.height, t='out_quad', d=.3).start(self.layout)
for node in list(treeview.iterate_all_nodes())[:]:
treeview.remove_node(node)
keys = list(widget.properties().keys())
keys.sort()
node = None
wk_widget = weakref.ref(widget)
for key in keys:
node = TreeViewProperty(key=key, widget_ref=wk_widget)
node.bind(is_selected=self.show_property)
try:
widget.bind(**{key: partial(
self.update_node_content, weakref.ref(node))})
except:
pass
treeview.add_node(node)
def update_node_content(self, node, *l):
node = node()
if node is None:
return
node.refresh = True
node.refresh = False
def keyboard_shortcut(self, win, scancode, *largs):
modifiers = largs[-1]
if scancode == 101 and modifiers == ['ctrl']:
self.activated = not self.activated
if self.activated:
self.inspect_enabled = True
return True
elif scancode == 27:
if self.inspect_enabled:
self.inspect_enabled = False
return True
if self.activated:
self.activated = False
return True
def show_property(self, instance, value, key=None, index=-1, *l):
# normal call: (tree node, focus, )
# nested call: (widget, prop value, prop key, index in dict/list)
if value is False:
return
content = None
if key is None:
# normal call
nested = False
widget = instance.widget
key = instance.key
prop = widget.property(key)
value = getattr(widget, key)
else:
# nested call, we might edit subvalue
nested = True
widget = instance
prop = None
dtype = None
if isinstance(prop, AliasProperty) or nested:
# trying to resolve type dynamically
if type(value) in (str, str):
dtype = 'string'
elif type(value) in (int, float):
dtype = 'numeric'
elif type(value) in (tuple, list):
dtype = 'list'
if isinstance(prop, NumericProperty) or dtype == 'numeric':
content = TextInput(text=str(value) or '', multiline=False)
content.bind(text=partial(
self.save_property_numeric, widget, key, index))
elif isinstance(prop, StringProperty) or dtype == 'string':
content = TextInput(text=value or '', multiline=True)
content.bind(text=partial(
self.save_property_text, widget, key, index))
elif (isinstance(prop, ListProperty) or
isinstance(prop, ReferenceListProperty) or
isinstance(prop, VariableListProperty) or
dtype == 'list'):
content = GridLayout(cols=1, size_hint_y=None)
content.bind(minimum_height=content.setter('height'))
for i, item in enumerate(value):
button = Button(text=repr(item), size_hint_y=None, height=44)
if isinstance(item, Widget):
button.bind(on_release=partial(self.highlight_widget, item,
False))
else:
button.bind(on_release=partial(self.show_property, widget,
item, key, i))
content.add_widget(button)
elif isinstance(prop, OptionProperty):
content = GridLayout(cols=1, size_hint_y=None)
content.bind(minimum_height=content.setter('height'))
for option in prop.options:
button = ToggleButton(
text=option,
state='down' if option == value else 'normal',
group=repr(content.uid), size_hint_y=None,
height=44)
button.bind(on_press=partial(
self.save_property_option, widget, key))
content.add_widget(button)
elif isinstance(prop, ObjectProperty):
if isinstance(value, Widget):
content = Button(text=repr(value))
content.bind(on_release=partial(self.highlight_widget, value))
elif isinstance(value, Texture):
content = Image(texture=value)
else:
content = Label(text=repr(value))
elif isinstance(prop, BooleanProperty):
state = 'down' if value else 'normal'
content = ToggleButton(text=key, state=state)
content.bind(on_release=partial(self.save_property_boolean, widget,
key, index))
self.content.clear_widgets()
if content:
self.content.add_widget(content)
def save_property_numeric(self, widget, key, index, instance, value):
try:
if index >= 0:
getattr(widget, key)[index] = float(instance.text)
else:
setattr(widget, key, float(instance.text))
except:
pass
def save_property_text(self, widget, key, index, instance, value):
try:
if index >= 0:
getattr(widget, key)[index] = instance.text
else:
setattr(widget, key, instance.text)
except:
pass
def save_property_boolean(self, widget, key, index, instance, ):
try:
value = instance.state == 'down'
if index >= 0:
getattr(widget, key)[index] = value
else:
setattr(widget, key, value)
except:
pass
def save_property_option(self, widget, key, instance, *l):
try:
setattr(widget, key, instance.text)
except:
pass
def _update_widget_tree_node(self, node, widget, is_open=False):
tree = self.widgettree
update_nodes = []
nodes = {}
for cnode in node.nodes[:]:
try:
nodes[cnode.widget] = cnode
except ReferenceError:
# widget no longer exists, just remove it
pass
tree.remove_node(cnode)
for child in widget.children:
if child is self:
continue
if child in nodes:
cnode = tree.add_node(nodes[child], node)
else:
cnode = tree.add_node(TreeViewWidget(
text=child.__class__.__name__, widget=child.proxy_ref,
is_open=is_open), node)
update_nodes.append((cnode, child))
return update_nodes
def update_widget_tree(self, *args):
if not hasattr(self, '_window_node') or not self._window_node:
self._window_node = self.widgettree.add_node(
TreeViewWidget(text='Window', widget=self.win, is_open=True))
nodes = self._update_widget_tree_node(self._window_node, self.win,
is_open=True)
while nodes:
ntmp = nodes[:]
nodes = []
for node in ntmp:
nodes += self._update_widget_tree_node(*node)
self.widgettree.update_selected_widget(self.widget)
[docs]def create_inspector(win, ctx, *l):
'''Create an Inspector instance attached to the *ctx* and bound to the
Window's :meth:`~kivy.core.window.WindowBase.on_keyboard` event for
capturing the keyboard shortcut.
:Parameters:
`win`: A :class:`Window <kivy.core.window.WindowBase>`
The application Window to bind to.
`ctx`: A :class:`~kivy.uix.widget.Widget` or subclass
The Widget to be inspected.
'''
# Dunno why, but if we are creating inspector within the start(), no lang
# rules are applied.
ctx.inspector = Inspector(win=win)
win.bind(children=ctx.inspector.on_window_children,
on_keyboard=ctx.inspector.keyboard_shortcut)
def start(win, ctx):
Clock.schedule_once(partial(create_inspector, win, ctx))
[docs]def stop(win, ctx):
'''Stop and unload any active Inspectors for the given *ctx*.'''
if hasattr(ctx, 'inspector'):
win.unbind(children=ctx.inspector.on_window_children,
on_keyboard=ctx.inspector.keyboard_shortcut)
win.remove_widget(ctx.inspector)
del ctx.inspector