Search code examples
python-2.7kivykivy-language

Python/Kivy : `up` and `down` Keyboard interface on Tree View in kivy


I want to use up and down Keyboard interface on Tree View.When i click on name then Tree View popup shows And up,down key working good.But i type something into TextInput for filter row after use up and down key for select row then it gives error AttributeError: 'NoneType' object has no attribute 'parent_node' .How to row select using up and down keys?

test.py

from kivy.uix.screenmanager import Screen
from kivy.app import App
from kivy.lang import Builder
from kivy.core.window import Window
from kivy.uix.popup import Popup
from kivy.uix.treeview import TreeView, TreeViewLabel, TreeViewNode
from kivy.uix.label import Label
from kivy.properties import ObjectProperty, ListProperty, StringProperty
Window.size = (500, 400)


def populate_tree_view(tree_view, parent, node):
    if parent is None:
        tree_node = tree_view.add_node(TreeViewLabel(text=node['node_id'],
                                                     is_open=True))
    else:
        tree_node = tree_view.add_node(TreeViewLabel(text=node['node_id'],
                                                     is_open=True), parent)

    for child_node in node['children']:
        populate_tree_view(tree_view, tree_node, child_node)


class TreeViewLabel(Label, TreeViewNode):
    pass


class TreeViewGroup(Popup):
    tree_view = ObjectProperty(None)
    tv = ObjectProperty(None)
    filter_text = StringProperty('')
    tree = ListProperty([])

    def __init__(self, **kwargs):
        super(TreeViewGroup, self).__init__(**kwargs)
        self.create_tree_view_root()
        rows = ['test{}'.format(i) for i in range(1, 20)]
        self.tree = [{'node_id': r, 'children': []} for r in rows]
        self.tv.bind(minimum_height=self.tree_view.setter('height'))
        self.create_tree_view_branch(self.tree)

    def create_tree_view_root(self):
        self.tv = TreeView(root_options=dict(text=""),
                           hide_root=False,
                           indent_level=4)

    def create_tree_view_branch(self, obj):
        for branch in obj:
            populate_tree_view(self.tv, None, branch)
        self.tree_view.add_widget(self.tv)

    def on_open(self, *args):
        self.filter_text = App.get_running_app().root.name.text
        self._request_keyboard()
        self.ti.focus = True

    def dismiss_callback(self):
        if self._keyboard is not None:
            self._keyboard.release()
        self.tree_view.clear_widgets()
        self.dismiss()
        App.get_running_app().root.name.focus = True

    def _request_keyboard(self):
        self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
        self._keyboard.bind(on_key_down=self._on_keyboard_down)
        if self.tv.selected_node is None:
            self.tv.select_node(self.tv.root.nodes[0])

    def _keyboard_closed(self):
        self._keyboard.unbind(on_key_down=self._on_keyboard_down)
        self._keyboard.release()
        self._keyboard = None

    def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
        node = self.tv.selected_node
        _, key = keycode
        if key in ('down', 'up'):
            parent = node.parent_node
            ix = parent.nodes.index(node)
            nx = ix+1 if key == 'down' else ix-1
            next_node = parent.nodes[nx % len(parent.nodes)]
            self.tv.select_node(next_node)
            self.scroll.scroll_to(next_node)
        elif key in ('enter', 'numpadenter'):
            App.get_running_app().root.name.text = node.text
            print(node.text)
            self.dismiss_callback()

        # Keycode is composed of an integer + a string
        # If we hit escape, release the keyboard
        if keycode[1] == 'escape':
            keyboard.release()

        # Return True to accept the key. Otherwise, it will be used by
        # the system.
        return True

    def filter(self, value):
        self.tree_view.clear_widgets()
        self.create_tree_view_root()
        filtered_tree = []
        for node in self.tree:
            if value.lower() in node['node_id'].lower():
                filtered_tree.append(node)
        self.create_tree_view_branch(filtered_tree)
        self._request_keyboard()
        self.ti.focus = True


class GroupScreen(Screen):
    name = ObjectProperty(None)
    popup = ObjectProperty(None)

    def display_groups(self, instance):
        if len(instance.text) > 0:
            if self.popup is None:
                self.popup = TreeViewGroup()
            self.popup.open()


class Group(App):
    def build(self):
        self.root = Builder.load_file('test.kv')
        return self.root


if __name__ == '__main__':
    Group().run()

test.kv

#:kivy 1.10.0

<TreeViewLabel>:
    color_selected: [1, 0, 0, 1]  if self.is_selected else [.1, .1, .1, 1]  # red
    on_touch_down:
        app.root.name.text = self.text
        app.root.popup.dismiss_callback()

<TreeviewGroup>:
    tree_view: tree_view
    title: "Select"
    title_size: 17
    size: 800, 800
    auto_dismiss: False
    scroll: scroll
    ti:ti

    BoxLayout
        orientation: "vertical"
        TextInput:
            id : ti
            text: root.filter_text
            size_hint_y: .13
            multiline: False
            on_text: root.filter(self.text)

        ScrollView:
            id: scroll
            size_hint: 1, .9

            BoxLayout:
                size_hint_y: None
                id: tree_view

        GridLayout:
            cols: 2
            row_default_height: '20dp'
            size_hint: .5, 0.1
            pos_hint: {'x': .25, 'y': 1}
            Button:
                text: 'Ok'
                on_release:
                    root.dismiss_callback()

            Button:
                text: 'Cancel'
                on_release:
                    root.dismiss_callback()

<CustomLabel@Label>:
    text_size: self.size
    valign: "middle"
    padding_x: 5

<SingleLineTextInput@TextInput>:
    multiline: False

<GreenButton@Button>:
    background_color: 1, 1, 1, 1
    size_hint_y: None
    height: self.parent.height * 0.150

GroupScreen:
    name: name

    GridLayout:
        cols: 2
        padding : 30,30
        spacing: 10, 10
        row_default_height: '40dp'

        CustomLabel:
            text: ' '

        CustomLabel:
            text: ' '

        CustomLabel:
            text: 'Name'

        SingleLineTextInput:
            id: name
            focus: True
            multiline: False
            on_text: root.display_groups(self)

        GreenButton:
            text: 'Ok'

        GreenButton:
            text: 'Cancel'
            on_press: app.stop()

Solution

  • Select a row - press ENTER or Mouse click

    Use up or down arrow to scroll, and press ENTER to select a specific row. Note: Alternatives, mouse click to select a specific row.

    Pass input string from Name to Filter

    To pass the whole word e.g. test, replace on_focus with on_text_validate.

    DRY - Don't Repeat Yourselves

    There are duplicate codes, I have turned them into modules for easy maintenance.

    def create_treeview_root(self):
    ...
    def create_treeview_branch(self, obj):
    ...
    def dismiss_callback(self):
    

    Snippet

    GroupScreen:
        ...
            SingleLineTextInput:
                id: name
                focus: True
                multiline: False
                on_text_validate: root.display_groups(self)
    

    Popup Events on_open

    Removed the binding in __init__ because the on_open is automatically binded and it is fired when the Popup is opened. Popup Events » API

    class TreeviewGroup(Popup):
        ...
    
        def __init__(self,obj, **kwargs):
            super(TreeviewGroup, self).__init__(**kwargs)
            ...
            self.bind(on_open=self.on_open)
    

    Example

    main.py

    from kivy.uix.screenmanager import Screen
    from kivy.app import App
    from kivy.lang import Builder
    from kivy.core.window import Window
    from kivy.uix.popup import Popup
    from kivy.uix.treeview import TreeView, TreeViewLabel, TreeViewNode
    from kivy.uix.label import Label
    from kivy.uix.textinput import TextInput
    from kivy.properties import ObjectProperty, ListProperty, StringProperty, NumericProperty
    Window.size = (500, 400)
    
    
    def populate_tree_view(tree_view, parent, node):
        if parent is None:
            tree_node = tree_view.add_node(TreeViewLabel(text=node['node_id'],
                                                         is_open=True))
        else:
            tree_node = tree_view.add_node(TreeViewLabel(text=node['node_id'],
                                                         is_open=True), parent)
    
        for child_node in node['children']:
            populate_tree_view(tree_view, tree_node, child_node)
    
    
    class CustomTextInput(TextInput):
    
        def do_cursor_movement(self, action, control=False, alt=False):
            if not self._lines:
                return
            if action in ('cursor_up', 'cursor_down'):
                App.get_running_app().root.popup._request_keyboard()
            return super(CustomTextInput, self).do_cursor_movement(action, control=control, alt=alt)
    
    
    class TreeViewLabel(Label, TreeViewNode):
        pass
    
    
    class TreeViewGroup(Popup):
        tree_view = ObjectProperty(None)
        filter_text_input = ObjectProperty(None)
        tv = ObjectProperty(None)
        filter_text = StringProperty('')
        tree = ListProperty([])
        obj = ObjectProperty(None)
    
        keycodes = {
            # specials keys
            'backspace': 8, 'tab': 9, 'enter': 13, 'rshift': 303, 'shift': 304,
            'alt': 308, 'rctrl': 306, 'lctrl': 305,
            'super': 309, 'alt-gr': 307, 'compose': 311, 'pipe': 310,
            'capslock': 301, 'escape': 27, 'spacebar': 32, 'pageup': 280,
            'pagedown': 281, 'end': 279, 'home': 278, 'left': 276, 'up':
            273, 'right': 275, 'down': 274, 'insert': 277, 'delete': 127,
            'numlock': 300, 'print': 144, 'screenlock': 145, 'pause': 19,
    
            # F1-15
            'f1': 282, 'f2': 283, 'f3': 284, 'f4': 285, 'f5': 286, 'f6': 287,
            'f7': 288, 'f8': 289, 'f9': 290, 'f10': 291, 'f11': 292, 'f12': 293,
            'f13': 294, 'f14': 295, 'f15': 296,
        }
    
        def __init__(self, **kwargs):
            super(TreeViewGroup, self).__init__(**kwargs)
            self._keyboard = None
    
            self.create_tree_view_root()
            rows = ['test{}'.format(i) for i in range(1, 20)]
            self.tree = [{'node_id': r, 'children': []} for r in rows]
            self.tv.bind(minimum_height=self.tree_view.setter('height'))
            self.create_tree_view_branch(self.tree)
    
        def create_tree_view_root(self):
            self.tv = TreeView(root_options=dict(text=""),
                               hide_root=False,
                               indent_level=4)
    
        def create_tree_view_branch(self, obj):
            for branch in obj:
                populate_tree_view(self.tv, None, branch)
            self.tree_view.add_widget(self.tv)
    
        def on_open(self, *args):
            self.obj = self.filter_text_input
            self.filter_text_input.focus = True
            self.filter_text = App.get_running_app().root.name.text
    
        def dismiss_callback(self):
            if self._keyboard is not None:
                self._keyboard.release()
            self.tree_view.clear_widgets()
            self.dismiss()
            App.get_running_app().root.name.focus = True
    
        def _request_keyboard(self):
            self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
            self._keyboard.bind(on_key_down=self._on_keyboard_down)
            if (self.tv.selected_node is None) \
                    and (len(self.tv.root.nodes) > 0):
                self.tv.select_node(self.tv.root.nodes[0])
            else:
                self.filter_text_input.focus = True
    
        def _keyboard_closed(self):
            self._keyboard.unbind(on_key_down=self._on_keyboard_down)
            self._keyboard.release()
            self._keyboard = None
    
        def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
            node = self.tv.selected_node
            _, key = keycode
            if key in ('down', 'up'):
                parent = node.parent_node
                ix = parent.nodes.index(node)
                nx = ix+1 if key == 'down' else ix-1
                next_node = parent.nodes[nx % len(parent.nodes)]
                self.tv.select_node(next_node)
                self.scroll.scroll_to(next_node)
            elif key in ('enter', 'numpadenter'):
                App.get_running_app().root.name.text = node.text
                print(node.text)
                self.dismiss_callback()
    
            # Keycode is composed of an integer + a string
            # If we hit escape, release the keyboard
            if keycode[1] == 'escape':
                keyboard.release()
    
            if self.string_to_keycode(key) == -1:
                self.filter_text += key
                self.obj.focus = True
    
            # Return True to accept the key. Otherwise, it will be used by
            # the system.
            return True
    
        def string_to_keycode(self, value):
            '''Convert a string to a keycode number according to the
            :attr:`TreeViewGroup.keycodes`. If the value is not found in the
            keycodes, it will return -1.
            '''
            return TreeViewGroup.keycodes.get(value, -1)
    
        def filter(self, value):
            self.tree_view.clear_widgets()
            self.create_tree_view_root()
            filtered_tree = []
            for node in self.tree:
                if value.lower() in node['node_id'].lower():
                    filtered_tree.append(node)
            self.create_tree_view_branch(filtered_tree)
    
    
    class GroupScreen(Screen):
        name = ObjectProperty(None)
        popup = ObjectProperty(None)
    
        def display_groups(self, instance):
            if len(instance.text) > 0:
                if self.popup is None:
                    self.popup = TreeViewGroup()
                self.popup.open()
    
    
    class Group(App):
        def build(self):
            self.root = Builder.load_file('test.kv')
            return self.root
    
    
    if __name__ == '__main__':
        Group().run()
    

    test.kv

    #:kivy 1.10.0
    
    <CustomTextInput>:
        size_hint_y: .13
        multiline: False
    
    <TreeViewLabel>:
        color_selected: [1, 0, 0, 1]  if self.is_selected else [.1, .1, .1, 1]  # red
        on_touch_down:
            app.root.name.text = self.text
            app.root.popup.dismiss_callback()
    
    <TreeviewGroup>:
        tree_view: tree_view
        filter_text_input: filter_text_input
        title: "Select"
        title_size: 17
        size: 800, 800
        auto_dismiss: False
        scroll: scroll
    
        BoxLayout
            orientation: "vertical"
    
            CustomTextInput:
                id: filter_text_input
                text: root.filter_text
                on_text:
                    root.filter_text = self.text
                    root.filter(self.text)
    
            ScrollView:
                id: scroll
                size_hint: 1, .9
    
                BoxLayout:
                    size_hint_y: None
                    id: tree_view
    
            GridLayout:
                cols: 2
                row_default_height: '20dp'
                size_hint: .5, 0.1
                pos_hint: {'x': .25, 'y': 1}
                Button:
                    text: 'Ok'
                    on_release:
                        root.dismiss_callback()
    
                Button:
                    text: 'Cancel'
                    on_release:
                        root.dismiss_callback()
    
    <CustomLabel@Label>:
        text_size: self.size
        valign: "middle"
        padding_x: 5
    
    <SingleLineTextInput@TextInput>:
        multiline: False
    
    <GreenButton@Button>:
        background_color: 1, 1, 1, 1
        size_hint_y: None
        height: self.parent.height * 0.150
    
    GroupScreen:
        name: name
    
        GridLayout:
            cols: 2
            padding : 30,30
            spacing: 10, 10
            row_default_height: '40dp'
    
            CustomLabel:
                text: ' '
    
            CustomLabel:
                text: ' '
    
            CustomLabel:
                text: 'Name'
    
            SingleLineTextInput:
                id: name
                focus: True
                multiline: False
                on_text: root.display_groups(self)
    
            GreenButton:
                text: 'Ok'
    
            GreenButton:
                text: 'Cancel'
                on_press: app.stop()
    

    Output

    Img01 - App Startup Img02 - Filter Popup Img03 - Scroll to bottom Img04 - Enter pressed Img05 - filter text Img06 - clicked OK button