Search code examples
python-2.7kivykivy-language

Python/Kivy : How to search child node in TreeView


I am using python-2.7 and kivy-1.9.0. When i run test.py then shows name TextInput . When i type into name then TreeView open.I added a TextInput for Filter text . But it work only for Parent node.When i type R then it doesn't show Reserves & Surplus in Results.It doesn't work for child node .
1.Can someone tell me that how to search also in child node ?

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.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 = [{'node_id': 'Capital Account',
                 'children': [{'node_id': 'Reserves & Surplus',
                               'children': [{'node_id': '1.1.1',
                                             'children': [{'node_id': '1.1.1.1',
                                                           'children': []}]},
                                            {'node_id': '1.1.2',
                                             'children': []},
                                            {'node_id': '1.1.3',
                                             'children': []}]},
                              {'node_id': '1.2',
                               'children': []}]},
                {'node_id': 'Current Assests',
                 'children': []}]


        self.tree = [{'node_id': r['node_id'], 'children': 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()

Solution

  • I never used kivy but the node search is done in the filter method and it doesn't search the children. So, replace it with the following:

    def filter(self, value):
        self.tree_view.clear_widgets()
        self.create_tree_view_root()
        filtered_tree = []
        v = value.lower() # the lowercase value
    
        def filter_children(parent): # a recursive inner function
            ch = parent['children']
            if not ch:
                return
            for node in ch:
                if v in node['node_id'].lower():
                    filtered_tree.append(node)
                filter_children(node)
    
        for node in self.tree:
            if v in node['node_id'].lower():
                filtered_tree.append(node)
            filter_children(node) # look into children
        self.create_tree_view_branch(filtered_tree)
    

    If you paste this code then pay attention to add the extra indentation of 4.