''' 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 weakref from functools import partial from itertools import chain from kivy.animation import Animation from kivy.logger import Logger from kivy.graphics.transformation import Matrix from kivy.clock import Clock from kivy.lang import Builder from kivy.factory import Factory from kivy.weakproxy import WeakProxy from kivy.properties import ( ObjectProperty, BooleanProperty, ListProperty, NumericProperty, StringProperty, OptionProperty, ReferenceListProperty, AliasProperty, VariableListProperty ) Builder.load_string(''' : 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 : 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(Factory.BoxLayout, Factory.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(Factory.Label, Factory.TreeViewNode): widget = ObjectProperty(None) class WidgetTree(Factory.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(Factory.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 = Factory.Color(1, 0, 0, .25) Factory.PushMatrix() self.gtransform = Factory.Transform(Matrix()) self.grect = Factory.Rectangle(size=(0, 0)) Factory.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, Factory.ModalView)), ( c for c in reversed(win_children) if not isinstance(c, Factory.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, *largs): 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 if type(widget) is WeakProxy: wk_widget = widget.__ref__ else: 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, *largs): 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 set(modifiers) & {'ctrl'} and not set( modifiers) & {'shift', 'alt', 'meta'}: 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, *largs): # 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 = Factory.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 = Factory.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 = Factory.GridLayout(cols=1, size_hint_y=None) content.bind(minimum_height=content.setter('height')) for i, item in enumerate(value): button = Factory.Button( text=repr(item), size_hint_y=None, height=44 ) if isinstance(item, Factory.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 = Factory.GridLayout(cols=1, size_hint_y=None) content.bind(minimum_height=content.setter('height')) for option in prop.options: button = Factory.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, Factory.Widget): content = Factory.Button(text=repr(value)) content.bind(on_release=partial(self.highlight_widget, value)) elif isinstance(value, Factory.Texture): content = Factory.Image(texture=value) else: content = Factory.Label(text=repr(value)) elif isinstance(prop, BooleanProperty): state = 'down' if value else 'normal' content = Factory.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, *largs): 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) def create_inspector(win, ctx, *largs): '''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 ` 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): ctx.ev_late_create = Clock.schedule_once( partial(create_inspector, win, ctx)) def stop(win, ctx): '''Stop and unload any active Inspectors for the given *ctx*.''' if hasattr(ctx, 'ev_late_create'): ctx.ev_late_create.cancel() del ctx.ev_late_create 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