diff --git a/doc/autobuild.py b/doc/autobuild.py index dd91bce34..cf5c28182 100644 --- a/doc/autobuild.py +++ b/doc/autobuild.py @@ -42,6 +42,7 @@ import kivy.graphics import kivy.graphics.shader import kivy.graphics.tesselator import kivy.animation +import kivy.modules.console import kivy.modules.keybinding import kivy.modules.monitor import kivy.modules.touchring diff --git a/kivy/modules/console.py b/kivy/modules/console.py new file mode 100644 index 000000000..ce8cf3c4e --- /dev/null +++ b/kivy/modules/console.py @@ -0,0 +1,1046 @@ +# coding=utf-8 +""" +Console +======= + +.. versionadded:: 1.9.1 + +Reboot of the old inspector, designed to be modular and keep concerns separated. +It also have a addons architecture that allow you to add a button, panel, or +more in the Console itself. + +.. warning:: + + This module works, but might fail in some cases. Please contribute! + +Usage +----- + +For normal module usage, please see the :mod:`~kivy.modules` documentation:: + + python main.py -m console + +Mouse navigation +---------------- + +When "Select" button is activated, you can: + +- 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) + +Keyboard navigation +------------------- + +- "Ctrl + e": toggle console +- "Escape": cancel widget lookup, then hide inspector view +- "Top": select the parent widget +- "Down": select the first children of the current selected widget +- "Left": select the previous following sibling +- "Right": select the next following sibling + +Additionnal informations +------------------------ + +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. + +Addons +------ + +Addons must be added to `Console.addons` before the first Clock tick of the +application, or before the create_console is called. You cannot add addons on +the fly currently. Addons are quite cheap until the Console is activated. Panel +are even cheaper, nothing is done until the user select it. + +By default, we provide multiple addons activated by default: + +- ConsoleAddonFps: display the FPS at the top-right +- ConsoleAddonSelect: activate the selection mode +- ConsoleAddonBreadcrumb: display the hierarchy of the current widget at the + bottom +- ConsoleAddonWidgetTree: panel to display the widget tree of the application +- ConsoleAddonWidgetPanel: panel to display the properties of the selected + widget + +If you need to add custom widget in the Console, please use either +:class:`ConsoleButton`, :class:`ConsoleToggleButton` or :class:`ConsoleLabel` + +An addon must inherit from the :class:`ConsoleAddon` class. + +For example, here is a simple addon for displaying the FPS at the top/right +of the Console:: + + from kivy.modules.console import Console, ConsoleAddon + + class ConsoleAddonFps(ConsoleAddon): + def init(self): + self.lbl = ConsoleLabel(text="0 Fps") + self.console.add_toolbar_widget(self.lbl, right=True) + + def activate(self): + Clock.schedule_interval(self.update_fps, 1 / 2.) + + def deactivated(self): + Clock.unschedule(self.update_fps) + + def update_fps(self, *args): + fps = Clock.get_fps() + self.lbl.text = "{} Fps".format(int(fps)) + + Console.register_addon(ConsoleAddonFps) + + +You can create addon that adds panels. Panel activation/deactivation are not +tied to the addon activation/deactivation, but on some cases, you can use the +same callback for deactivating the addon and the panel. Here is a simple About +panel addon:: + + from kivy.modules.console import Console, ConsoleAddon, ConsoleLabel + + class ConsoleAddonAbout(ConsoleAddon): + def init(self): + self.console.add_panel("About", self.panel_activate, + self.panel_deactivate) + + def panel_activate(self): + self.console.bind(widget=self.update_content) + self.update_content() + + def panel_deactivate(self): + self.console.unbind(widget=self.update_content) + + def deactivate(self): + self.panel_deactivate() + + def update_content(self, *args): + widget = self.console.widget + if not widget: + return + text = "Selected widget is: {!r}".format(widget) + lbl = ConsoleLabel(text=text) + self.console.set_content(lbl) + + Console.register_addon(ConsoleAddonAbout) + +""" + +__all__ = ("start", "stop", "create_console", "Console", "ConsoleAddon", + "ConsoleButton", "ConsoleToggleButton", "ConsoleLabel") + +import kivy +kivy.require('1.0.9') + +import weakref +from functools import partial +from itertools import chain +from kivy.logger import Logger +from kivy.uix.widget import Widget +from kivy.uix.button import Button +from kivy.uix.togglebutton import ToggleButton +from kivy.uix.label import Label +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.relativelayout import RelativeLayout +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(""" +: + size_hint: (1, None) if self.mode == "docked" else (None, None) + height: dp(250) + + canvas: + Color: + rgb: .185, .18, .18 + Rectangle: + size: self.size + Color: + rgb: .3, .3, .3 + Rectangle: + pos: 0, self.height - dp(48) + size: self.width, dp(48) + + GridLayout: + cols: 1 + id: layout + + GridLayout: + id: toolbar + rows: 1 + height: "48dp" + size_hint_y: None + padding: "4dp" + spacing: "4dp" + + + RelativeLayout: + id: content + + +: + size_hint_x: None + width: "10dp" + +: + size_hint_x: None + width: self.texture_size[0] + dp(20) + + +: + size_hint_y: None + height: "48dp" + canvas: + Color: + rgb: .3, .3, .3 + Rectangle: + size: self.size + ScrollView: + id: sv + do_scroll_y: False + GridLayout: + id: stack + rows: 1 + size_hint_x: None + width: self.minimum_width + padding: "4dp" + spacing: "4dp" + +: + height: max(dp(48), 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) + +: + ScrollView: + scroll_type: ['bars', 'content'] + bar_width: 10 + + ConsoleAddonWidgetTreeImpl: + 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.console.highlight_widget(args[1]) + +<-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.) + +""") + + +def ignore_exception(f): + def f2(*args, **kwargs): + try: + return f(*args, **kwargs) + except: + pass + + return f2 + + +class TreeViewProperty(BoxLayout, TreeViewNode): + key = ObjectProperty(None, allownone=True) + refresh = BooleanProperty(False) + widget_ref = ObjectProperty(None, allownone=True) + + def _get_widget(self): + wr = self.widget_ref + if wr is None: + return None + wr = wr() + if wr is None: + self.widget_ref = None + return None + return wr + + widget = AliasProperty(_get_widget, None, bind=('widget_ref', )) + + +class ConsoleButton(Button): + """Button specialized for the Console""" + pass + + +class ConsoleToggleButton(ToggleButton): + """ToggleButton specialized for the Console""" + pass + + +class ConsoleLabel(Label): + """LabelButton specialized for the Console""" + pass + + +class ConsoleAddonSeparator(Widget): + pass + + +class ConsoleAddon(object): + """Base class for implementing addons""" + + #: Console instance + console = None + + def __init__(self, console): + super(ConsoleAddon, self).__init__() + self.console = console + self.init() + + def init(self): + """Method called when the addon is instanciated by the Console + """ + pass + + def activate(self): + """Method called when the addon is activated by the console + (when the console is displayed)""" + pass + + def deactivate(self): + """Method called when the addon is deactivated by the console + (when the console is hidden) + """ + pass + + +class ConsoleAddonMode(ConsoleAddon): + def init(self): + btn = ConsoleToggleButton(text=u"Docked") + self.console.add_toolbar_widget(btn) + + +class ConsoleAddonSelect(ConsoleAddon): + def init(self): + self.btn = ConsoleToggleButton(text=u"Select") + self.btn.bind(state=self.on_button_state) + self.console.add_toolbar_widget(self.btn) + self.console.bind(inspect_enabled=self.on_inspect_enabled) + + def on_inspect_enabled(self, instance, value): + self.btn.state = "down" if value else "normal" + + def on_button_state(self, instance, value): + self.console.inspect_enabled = (value == "down") + + +class ConsoleAddonFps(ConsoleAddon): + def init(self): + self.lbl = ConsoleLabel(text="0 Fps") + self.console.add_toolbar_widget(self.lbl, right=True) + + def activate(self): + Clock.schedule_interval(self.update_fps, 1 / 2.) + + def deactivated(self): + Clock.unschedule(self.update_fps) + + def update_fps(self, *args): + fps = Clock.get_fps() + self.lbl.text = "{} Fps".format(int(fps)) + + +class ConsoleAddonBreadcrumbView(RelativeLayout): + widget = ObjectProperty(None, allownone=True) + parents = [] + + def on_widget(self, instance, value): + stack = self.ids.stack + + # determine if we can just highlight the current one + # or if we need to rebuild the breadcrumb + prefs = [btn.widget_ref() for btn in self.parents] + if value in prefs: + # ok, so just toggle this one instead. + index = prefs.index(value) + for btn in self.parents: + btn.state = "normal" + self.parents[index].state = "down" + return + + # we need to rebuild the breadcrumb. + stack.clear_widgets() + if not value: + return + widget = value + parents = [] + while True: + btn = ConsoleButton(text=widget.__class__.__name__) + btn.widget_ref = weakref.ref(widget) + btn.bind(on_release=self.highlight_widget) + parents.append(btn) + if widget == widget.parent: + break + widget = widget.parent + for btn in reversed(parents): + stack.add_widget(btn) + self.ids.sv.scroll_x = 1 + self.parents = parents + btn.state = "down" + + def highlight_widget(self, instance): + self.console.widget = instance.widget_ref() + + +class ConsoleAddonBreadcrumb(ConsoleAddon): + def init(self): + self.view = ConsoleAddonBreadcrumbView() + self.view.console = self.console + self.console.ids.layout.add_widget(self.view) + + def activate(self): + self.console.bind(widget=self.update_content) + self.update_content() + + def deactivate(self): + self.console.unbind(widget=self.update_content) + + def update_content(self, *args): + self.view.widget = self.console.widget + + +class ConsoleAddonWidgetPanel(ConsoleAddon): + def init(self): + self.console.add_panel("Properties", self.panel_activate, + self.deactivate) + + def panel_activate(self): + self.console.bind(widget=self.update_content) + self.update_content() + + def deactivate(self): + self.console.unbind(widget=self.update_content) + + def update_content(self, *args): + widget = self.console.widget + if not widget: + return + + from kivy.uix.scrollview import ScrollView + self.root = root = BoxLayout() + self.sv = sv = ScrollView(scroll_type=["bars", "content"]) + treeview = TreeView(hide_root=True, size_hint_y=None) + treeview.bind(minimum_height=treeview.setter("height")) + keys = list(widget.properties().keys()) + keys.sort() + node = None + wk_widget = weakref.ref(widget) + for key in keys: + text = '%s' % key + node = TreeViewProperty(text=text, 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) + + root.add_widget(sv) + sv.add_widget(treeview) + self.console.set_content(root) + + 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 + + console = self.console + 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 dynamicly + 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(console.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(console.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.root.clear_widgets() + self.root.add_widget(self.sv) + if content: + self.root.add_widget(content) + + @ignore_exception + def save_property_numeric(self, widget, key, index, instance, value): + if index >= 0: + getattr(widget, key)[index] = float(instance.text) + else: + setattr(widget, key, float(instance.text)) + + @ignore_exception + def save_property_text(self, widget, key, index, instance, value): + if index >= 0: + getattr(widget, key)[index] = instance.text + else: + setattr(widget, key, instance.text) + + @ignore_exception + def save_property_boolean(self, widget, key, index, instance, ): + value = instance.state == 'down' + if index >= 0: + getattr(widget, key)[index] = value + else: + setattr(widget, key, value) + + @ignore_exception + def save_property_option(self, widget, key, instance, *l): + setattr(widget, key, instance.text) + + +class TreeViewWidget(Label, TreeViewNode): + widget = ObjectProperty(None) + + +class ConsoleAddonWidgetTreeImpl(TreeView): + selected_widget = ObjectProperty(None, allownone=True) + + __events__ = ('on_select_widget', ) + + def __init__(self, **kwargs): + super(ConsoleAddonWidgetTreeImpl, 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 None + + 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(ConsoleAddonWidgetTreeImpl, 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 ConsoleAddonWidgetTreeView(RelativeLayout): + widget = ObjectProperty(None, allownone=True) + _window_node = None + + def _update_widget_tree_node(self, node, widget, is_open=False): + tree = self.ids.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 isinstance(child, Console): + 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): + win = self.console.win + if not self._window_node: + self._window_node = self.ids.widgettree.add_node( + TreeViewWidget(text="Window", + widget=win, + is_open=True)) + + nodes = self._update_widget_tree_node(self._window_node, win, + is_open=True) + while nodes: + ntmp = nodes[:] + nodes = [] + for node in ntmp: + nodes += self._update_widget_tree_node(*node) + + self.ids.widgettree.update_selected_widget(self.widget) + + +class ConsoleAddonWidgetTree(ConsoleAddon): + def init(self): + self.content = None + self.console.add_panel("Tree", self.panel_activate, self.deactivate, + self.panel_refresh) + + def panel_activate(self): + self.console.bind(widget=self.update_content) + self.update_content() + + def deactivate(self): + if self.content: + self.content.widget = None + self.content.console = None + self.console.unbind(widget=self.update_content) + + def update_content(self, *args): + widget = self.console.widget + if not self.content: + self.content = ConsoleAddonWidgetTreeView() + self.content.console = self.console + self.content.widget = widget + self.content.update_widget_tree() + self.console.set_content(self.content) + + def panel_refresh(self): + if self.content: + self.content.update_widget_tree() + + +class Console(RelativeLayout): + """Console interface + + This widget is created by create_console(), when the module is loaded. + During that time, you can add addons on the console to extend the + functionnalities, or add your own application stats / debugging module. + """ + + #: Array of addons that will be created at Console creation + addons = [ # ConsoleAddonMode, + ConsoleAddonSelect, ConsoleAddonFps, ConsoleAddonWidgetPanel, + ConsoleAddonWidgetTree, ConsoleAddonBreadcrumb + ] + + #: Display mode of the Console, either docked at the bottom, or as a + #: floating window. + mode = OptionProperty("docked", options=["docked", "floated"]) + + #: Current widget beeing selected + widget = ObjectProperty(None, allownone=True) + + #: Indicate if the inspector inspection is enabled. If yes, the next + #: touch down will select a the widget under the touch + inspect_enabled = BooleanProperty(False) + + #: True if the Console is activated (showed) + activated = BooleanProperty(False) + + def __init__(self, **kwargs): + super(Console, self).__init__(**kwargs) + self.avoid_bring_to_top = False + self.win = kwargs.get('win') + 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) + + # instanciate all addons + self._toolbar = {"left": [], "panels": [], "right": []} + self._addons = [] + self._panel = None + for addon in self.addons: + instance = addon(self) + self._addons.append(instance) + self._init_toolbar() + # select the first panel + self._panel = self._toolbar["panels"][0] + self._panel.state = "down" + self._panel.cb_activate() + + def _init_toolbar(self): + toolbar = self.ids.toolbar + for key in ("left", "panels", "right"): + if key == "right": + toolbar.add_widget(Widget()) + for el in self._toolbar[key]: + toolbar.add_widget(el) + if key != "right": + toolbar.add_widget(ConsoleAddonSeparator()) + + @classmethod + def register_addon(cls, addon): + cls.addons.append(addon) + + def add_toolbar_widget(self, widget, right=False): + """Add a widget in the top left toolbar of the Console. + Use `right=True` if you wanna add the widget at the right instead. + """ + key = "right" if right else "left" + self._toolbar[key].append(widget) + + def remove_toolbar_widget(self, widget): + """Remove a widget from the toolbar + """ + self.ids.toolbar.remove_widget(widget) + + def add_panel(self, name, cb_activate, cb_deactivate, cb_refresh=None): + """Add a new panel in the Console. + + - `cb_activate` is a callable that will be called when the panel is + activated by the user. + + - `cb_deactivate` is a callable that will be called when the panel is + deactivated or when the console will hide. + + - `cb_refresh` is an optionnal callable that is called if the user + click again on the button for display the panel + + When activated, it's up to the panel to display a content in the + Console by using :meth:`set_content`. + """ + btn = ConsoleToggleButton(text=name) + btn.cb_activate = cb_activate + btn.cb_deactivate = cb_deactivate + btn.cb_refresh = cb_refresh + btn.bind(on_press=self._activate_panel) + self._toolbar["panels"].append(btn) + + def _activate_panel(self, instance): + if self._panel != instance: + self._panel.cb_deactivate() + self._panel.state = "normal" + self.ids.content.clear_widgets() + self._panel = instance + self._panel.cb_activate() + self._panel.state = "down" + else: + self._panel.state = "down" + if self._panel.cb_refresh: + self._panel.cb_refresh() + + def set_content(self, content): + """Replace the Console content with a new one. + """ + self.ids.content.clear_widgets() + self.ids.content.add_widget(content) + + def on_touch_down(self, touch): + ret = super(Console, 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 + ret = True + else: + ret = self.collide_point(*touch.pos) + return ret + + def on_touch_move(self, touch): + ret = super(Console, 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(Console, 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: + 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): + """Select a widget from a x/y window coordinate. + This is mostly used internally when Select mode is activated + """ + 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 reversed(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, *largs): + # no widget to highlight, reduce rectangle to 0, 0 + self.widget = widget + if not widget: + self.grect.size = 0, 0 + + 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 pick(self, widget, x, y): + """Pick a widget at x/y, given a root `widget` + """ + 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 activated: + self._activate_console() + else: + self._deactivate_console() + + def _activate_console(self): + if not self in self.win.children: + self.win.add_widget(self) + self.y = 0 + for addon in self._addons: + addon.activate() + Logger.info('Console: console activated') + + def _deactivate_console(self): + for addon in self._addons: + addon.deactivate() + self.grect.size = 0, 0 + self.y = -self.height + self.widget = None + self.inspect_enabled = False + #self.win.remove_widget(self) + self._window_node = None + Logger.info('Console: console deactivated') + + 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 + + if not self.activated or not self.widget: + return + + if scancode == 273: # top + self.widget = self.widget.parent + + elif scancode == 274: # down + filtered_children = [c for c in self.widget.children + if not isinstance(c, Console)] + if filtered_children: + self.widget = filtered_children[0] + + elif scancode == 276: # left + parent = self.widget.parent + filtered_children = [c for c in parent.children + if not isinstance(c, Console)] + index = filtered_children.index(self.widget) + index = max(0, index - 1) + self.widget = filtered_children[index] + + elif scancode == 275: # right + parent = self.widget.parent + filtered_children = [c for c in parent.children + if not isinstance(c, Console)] + index = filtered_children.index(self.widget) + index = min(len(filtered_children) - 1, index + 1) + self.widget = filtered_children[index] + + +def create_console(win, ctx, *l): + ctx.console = Console(win=win) + win.bind(children=ctx.console.on_window_children, + on_keyboard=ctx.console.keyboard_shortcut) + + +def start(win, ctx): + """Create an Console instance attached to the *ctx* and bound to the + Windows :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. + + """ + Clock.schedule_once(partial(create_console, win, ctx)) + + +def stop(win, ctx): + """Stop and unload any active Inspectors for the given *ctx*.""" + if hasattr(ctx, "console"): + win.unbind(children=ctx.console.on_window_children, + on_keyboard=ctx.console.keyboard_shortcut) + win.remove_widget(ctx.console) + del ctx.console