0

I am trying to build a Gallery App based on KivyMD FileManager class. The layout consists of three columns and images are placed in these columns.

The problem is that when I click on images (they are IconButtons) to add small click icons on the left-bottom corner of the images, some more click icons are added to other images in the same column. I think it occurs because the layout repeats the same instances after a few cycles.

Click icon added as the button (image) pressed
Unexpected click icon appears in another row (third row after the clicked row)

The pseudocode is as follows:


import os
import threading
import time

from os import listdir
from os.path import join, isfile
from pathlib import Path

import PIL
from PIL import ImageOps
from kivy import Logger
from kivy.app import App
from kivy.clock import mainthread
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import OptionProperty, ListProperty, BooleanProperty, StringProperty
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.image import AsyncImage
from kivy.uix.recycleview import RecycleView
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.scrollview import ScrollView
from kivymd import images_path
from kivymd.app import MDApp
from kivymd.toast import toast
from PIL import Image as PILImage
from kivymd.uix.button import MDIconButton
from kivymd.uix.label import MDIcon
from kivymd.uix.toolbar import MDToolbar

Builder.load_string('''
#:import os os
<MyToolBar>:
    id: toolbar
    elevation: 10
    pos_hint:{'top':1}
    size_hint_y: 0.1
    md_bg_color: 0/255, 176/255, 240/255, 1
    specific_text_color: 1, 1, 1, 1
<RV>:
    id: rv
    key_viewclass: 'viewclass'
    key_size: 'height'
    bar_width: dp(4)
    bar_color: app.theme_cls.primary_color
    #on_scroll_stop: root._update_list_images()
    pos_hint: {'top':0.9}
    size_hint_y: 0.9
    
    RecycleBoxLayout:
        default_size: None, dp(500)
        default_size_hint: 1, None
        size_hint_y: None
        height: self.minimum_height
        orientation: 'vertical'
        
<LabelContent@MDLabel>
    size_hint_y: None
    height: self.texture_size[1]
    shorten: True
    shorten_from: 'center'
    halign: 'center'
    text_size: self.width, None
    
<BodyManagerWithPrevious>
    id: bodymanager
    paths: []
    path: ''
    type: 'folder'
    events_callback: lambda x: None
    orientation: 'vertical'
    
    MDGridLayout:
        id: grid_box
        cols: 3
        row_default_height: (self.width - self.cols * self.spacing[0]) / self.cols
        row_force_default: True
        adaptive_height: True
        padding: dp(4), dp(-4)
        spacing: dp(4), dp(4)
        #pos_hint: {'top':1}

        BoxLayout:
            orientation: 'vertical'
            IconButton:
                size_hint_y: None
                height: dp(300) if self.source and os.path.split(self.source)[1] == "folder.png" else root.width /3
                source: root.get_source(root.type, root.paths, 1)
                on_release: root.events_callback(path=root.get_source(root.type, root.paths, 1),instance=self)
            MDIcon:
                icon: ''
                pos: self.parent.children[1].pos

        BoxLayout:
            orientation: 'vertical'
            IconButton:
                size_hint_y: None
                height: dp(300) if self.source and os.path.split(self.source)[1] == "folder.png" else root.width /3
                source: root.get_source(root.type, root.paths, 2)
                on_release: root.events_callback(path=root.get_source(root.type, root.paths, 2), instance=self)
            MDIcon:
                icon: ''
                pos: self.parent.children[1].pos
        BoxLayout:
            orientation: 'vertical'
            IconButton:
                size_hint_y: None
                height: dp(300) if self.source and os.path.split(self.source)[1] == "folder.png" else root.width /3
                source: root.get_source(root.type, root.paths, 3)
                on_release: root.events_callback(path=root.get_source(root.type, root.paths, 3),instance=self)
            MDIcon:
                icon: ''
                pos: self.parent.children[1].pos
''')


class IconButton(ButtonBehavior, AsyncImage):
    allow_stretch = BooleanProperty()
    clicked = BooleanProperty()


class MyToolBar(MDToolbar):
    pass


class RV(RecycleView):
    search = OptionProperty("all", options=["all", "files"])
    ext = ListProperty()
    use_access = BooleanProperty(True)

    def __init__(self, path, **kwargs):
        super(RV, self).__init__(**kwargs)

        self.ext = [".png", ".jpg", ".jpeg"]
        self.app = MDApp.get_running_app()

        dirs, files = self.get_content(path)

        threading.Thread(target=self._create_previous, args=(path,)).start()

        split_files = self._split_list(files, 3)

        manager_list = []
        app = MDApp.get_running_app()
        for list_files in list(split_files):
            manager_list.append(
                {
                    "viewclass": "BodyManagerWithPrevious",
                    "path": path,
                    "paths": list_files,
                    "type": "files",
                    "height": app.root.width / 3,
                    "events_callback": app.add_checkicon
                })
        self.data = manager_list
        #[{'source': x} for x in img_source]

    def count_ext(self, path):
        ext = os.path.splitext(path)[1]
        if ext != "":
            # print(self.ext)
            if ext.lower() in self.ext or ext.upper() in self.ext:
                return True
        return False

    def _create_previous(self, path):
        if "r" not in self.get_access_string(path):
            toast("PermissionError")
            return

        for image in os.listdir(path):
            _path = os.path.join(path, image)
            if os.path.isfile(_path):
                if self.count_ext(_path):
                    path_to_thumb = os.path.join(
                        '/home/username/Desktop', "thumb", f"thumb_{image}"
                    )
                    if not os.path.exists(path_to_thumb):
                        im = PILImage.open(_path)
                        im = ImageOps.fit(im, (200,200), method=0, bleed=0.0, centering=(0.5, 0.5))
                        im.thumbnail((200, 200))
                        im.save(path_to_thumb, "PNG")

    def get_access_string(self, path):
        access_string = ""
        if self.use_access:
            access_data = {"r": os.R_OK, "w": os.W_OK, "x": os.X_OK}
            for access in access_data.keys():
                access_string += (
                    access if os.access(path, access_data[access]) else "-"
                )
        return access_string

    def get_content(self, path):
        """Returns a list of the type [[Folder List], [file list]]."""
        print(path)
        try:
            files = []
            dirs = []
            onlyfiles1 = [join(path, f) for f in listdir(path) if isfile(join(path, f))
                          and f.endswith(('JPG', 'png', ".jpg", ".jpeg"))]
            print(onlyfiles1)
            onlyfiles1.sort(key=os.path.getmtime)
            onlyfiles1.reverse()
            print(onlyfiles1)
            for each in onlyfiles1:
                content = each.split('/')[-1]
                if os.path.isdir(os.path.join(path, content)):
                    if self.search == "all" or self.search == "dirs":
                        dirs.append(content)
                else:
                    if self.search == "all" or self.search == "files":
                        if len(self.ext) != 0:
                            try:
                                if self.count_ext(content):
                                    files.append(
                                        os.path.join(
                                            '/home/username/Desktop',
                                            "thumb",
                                            f"thumb_{content}",
                                        )
                                    )

                            except IndexError:
                                pass
                        else:
                            files.append(content)
            return dirs, files
        except OSError:
            self.history.pop()
            return None, None

    def _update_list_images(self):
        # self.refresh_from_viewport()
        self.refresh_from_layout()

    def _split_list(self, l, n):
        if l:
            n = max(1, n)
            return (l[i : i + n] for i in range(0, len(l), n))
        else:
            return []


class BodyManagerWithPrevious(BoxLayout):
    def get_source(self, source_type, paths, index):
        if len(paths) >= index:
            source = paths[index - 1]
        else:
            source = f"{images_path}transparent.png"
        return source


class TestApp(MDApp):
    check_dict = {}
    iconcuk = StringProperty()
    instance_nums = []

    def build(self):
        self.sm = ScreenManager()
        self.sm.add_widget(Screen(name='Screen 1'))
        self.sm.add_widget(Screen(name='Screen 2'))
        wid = self.sm.get_screen('Screen 1')
        wid.add_widget(Button(text='Screen 1', on_release=self.change))
        return self.sm

    def change(self,*kwargs):
        self.sm.current = 'Screen 2'
        self.s2 = self.sm.get_screen('Screen 2')
        self.s2.add_widget(MyToolBar())
        self.rv = RV(path = 'Directory Path')
        self.s2.add_widget(self.rv)


    def add_checkicon(self, path, instance):

        if instance.parent.children[0].icon == 'check-bold':
            if instance.source in self.check_dict:
                instance.parent.children[0].icon = ''
                self.check_dict.pop(path)
            print(self.check_dict)
        else:
            if instance.source not in self.check_dict:
                instance.parent.children[0].icon = 'check-bold'
                self.check_dict[path] = instance
            print(self.check_dict)


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

Any help appreciated a lot!

  • I handled the issue by using SelectableRecycleBoxLayout. Whenever an image is selected, I add an icon inside the image layout instead of changing the color of its background. This way icons are being kept even though they don't appear on the screen. – Fazilet Gokbudak Sep 11 '20 at 05:53

1 Answers1

0

This is the standard behavior of the RecycleView widget - https://kivy.org/doc/stable/api-kivy.uix.recycleview.html#module-kivy.uix.recycleview

Xyanight
  • 1,315
  • 1
  • 7
  • 10