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!