globalforest/kivymd/uix/filemanager.py

659 lines
19 KiB
Python
Raw Normal View History

2020-10-14 00:19:43 -04:00
"""
Components/File Manager
=======================
A simple manager for selecting directories and files.
Usage
-----
.. code-block:: python
path = '/' # path to the directory that will be opened in the file manager
file_manager = MDFileManager(
exit_manager=self.exit_manager, # function called when the user reaches directory tree root
select_path=self.select_path, # function called when selecting a file/directory
)
file_manager.show(path)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/file-manager.png
:align: center
Or with ``preview`` mode:
.. code-block:: python
file_manager = MDFileManager(
exit_manager=self.exit_manager,
select_path=self.select_path,
preview=True,
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/file-manager-previous.png
:align: center
.. warning:: The `preview` mode is intended only for viewing images and will
not display other types of files.
Example
-------
.. code-block:: python
from kivy.core.window import Window
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.filemanager import MDFileManager
from kivymd.toast import toast
KV = '''
BoxLayout:
orientation: 'vertical'
MDToolbar:
title: "MDFileManager"
left_action_items: [['menu', lambda x: None]]
elevation: 10
FloatLayout:
MDRoundFlatIconButton:
text: "Open manager"
icon: "folder"
pos_hint: {'center_x': .5, 'center_y': .6}
on_release: app.file_manager_open()
'''
class Example(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
Window.bind(on_keyboard=self.events)
self.manager_open = False
self.file_manager = MDFileManager(
exit_manager=self.exit_manager,
select_path=self.select_path,
preview=True,
)
def build(self):
return Builder.load_string(KV)
def file_manager_open(self):
self.file_manager.show('/') # output manager to the screen
self.manager_open = True
def select_path(self, path):
'''It will be called when you click on the file name
or the catalog selection button.
:type path: str;
:param path: path to the selected directory or file;
'''
self.exit_manager()
toast(path)
def exit_manager(self, *args):
'''Called when the user reaches the root of the directory tree.'''
self.manager_open = False
self.file_manager.close()
def events(self, instance, keyboard, keycode, text, modifiers):
'''Called when buttons are pressed on the mobile device.'''
if keyboard in (1001, 27):
if self.manager_open:
self.file_manager.back()
return True
Example().run()
"""
__all__ = ("MDFileManager",)
import locale
import os
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import (
BooleanProperty,
ListProperty,
NumericProperty,
ObjectProperty,
OptionProperty,
StringProperty,
)
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.modalview import ModalView
from kivymd import images_path
from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import CircularRippleBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.floatlayout import MDFloatLayout
from kivymd.uix.list import BaseListItem, ContainerSupport
from kivymd.utils.fitimage import FitImage
ACTIVITY_MANAGER = """
#:import os os
<BodyManager@BoxLayout>
icon: "folder"
path: ""
background_normal: ""
background_down: ""
dir_or_file_name: ""
_selected: False
events_callback: lambda x: None
orientation: "vertical"
ModifiedOneLineIconListItem:
text: root.dir_or_file_name
bg_color: self.theme_cls.bg_darkest if root._selected else self.theme_cls.bg_normal
on_release: root.events_callback(root.path, root)
IconLeftWidget:
icon: root.icon
theme_text_color: "Custom"
text_color: self.theme_cls.primary_color
MDSeparator:
<LabelContent@MDLabel>
size_hint_y: None
height: self.texture_size[1]
shorten: True
shorten_from: "center"
halign: "center"
text_size: self.width, None
<BodyManagerWithPreview>
name: ""
path: ""
realpath: ""
type: "folder"
events_callback: lambda x: None
_selected: False
orientation: "vertical"
size_hint_y: None
hright: root.height
padding: dp(20)
IconButton:
mipmap: True
source: root.path
bg_color: app.theme_cls.bg_darkest if root._selected else app.theme_cls.bg_normal
on_release:
root.events_callback(\
os.path.join(root.path if root.type != "folder" else root.realpath, \
root.name), root)
LabelContent:
text: root.name
<FloatButton>
anchor_x: "right"
anchor_y: "bottom"
size_hint_y: None
height: dp(56)
padding: dp(10)
MDFloatingActionButton:
size_hint: None, None
size:dp(56), dp(56)
icon: root.icon
opposite_colors: True
elevation: 8
on_release: root.callback()
md_bg_color: root.md_bg_color
<MDFileManager>
md_bg_color: root.theme_cls.bg_normal
BoxLayout:
orientation: "vertical"
spacing: dp(5)
MDToolbar:
id: toolbar
title: root.current_path
right_action_items: [["close-box", lambda x: root.exit_manager(1)]]
left_action_items: [["chevron-left", lambda x: root.back()]]
elevation: 10
RecycleView:
id: rv
key_viewclass: "viewclass"
key_size: "height"
bar_width: dp(4)
bar_color: root.theme_cls.primary_color
#on_scroll_stop: root._update_list_images()
RecycleGridLayout:
padding: dp(10)
cols: 3 if root.preview else 1
default_size: None, dp(48)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: "vertical"
<ModifiedOneLineIconListItem>
BoxLayout:
id: _left_container
size_hint: None, None
x: root.x + dp(16)
y: root.y + root.height / 2 - self.height / 2
size: dp(48), dp(48)
"""
class BodyManagerWithPreview(MDBoxLayout):
"""Base class for folder icons and thumbnails images in ``preview`` mode."""
class IconButton(CircularRippleBehavior, ButtonBehavior, FitImage):
"""Folder icons/thumbnails images in ``preview`` mode."""
class FloatButton(AnchorLayout):
callback = ObjectProperty()
md_bg_color = ListProperty([1, 1, 1, 1])
icon = StringProperty()
class ModifiedOneLineIconListItem(ContainerSupport, BaseListItem):
_txt_left_pad = NumericProperty("72dp")
_txt_top_pad = NumericProperty("16dp")
_txt_bot_pad = NumericProperty("15dp")
_num_lines = 1
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.height = dp(48)
class MDFileManager(ThemableBehavior, MDFloatLayout):
icon = StringProperty("check")
"""
The icon that will be used on the directory selection button.
:attr:`icon` is an :class:`~kivy.properties.StringProperty`
and defaults to `check`.
"""
icon_folder = StringProperty(f"{images_path}folder.png")
"""
The icon that will be used for folder icons when using ``preview = True``.
:attr:`icon` is an :class:`~kivy.properties.StringProperty`
and defaults to `check`.
"""
exit_manager = ObjectProperty(lambda x: None)
"""
Function called when the user reaches directory tree root.
:attr:`exit_manager` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `lambda x: None`.
"""
select_path = ObjectProperty(lambda x: None)
"""
Function, called when selecting a file/directory.
:attr:`select_path` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `lambda x: None`.
"""
ext = ListProperty()
"""
List of file extensions to be displayed in the manager.
For example, `['.py', '.kv']` - will filter out all files,
except python scripts and Kv Language.
:attr:`ext` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
search = OptionProperty("all", options=["all", "dirs", "files"])
"""
It can take the values 'all' 'dirs' 'files' - display only directories
or only files or both them. By default, it displays folders, and files.
Available options are: `'all'`, `'dirs'`, `'files'`.
:attr:`search` is an :class:`~kivy.properties.OptionProperty`
and defaults to `all`.
"""
current_path = StringProperty(os.getcwd())
"""
Current directory.
:attr:`current_path` is an :class:`~kivy.properties.StringProperty`
and defaults to `/`.
"""
use_access = BooleanProperty(True)
"""
Show access to files and directories.
:attr:`use_access` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `True`.
"""
preview = BooleanProperty(False)
"""
Shows only image previews.
:attr:`preview` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
show_hidden_files = BooleanProperty(False)
"""
Shows hidden files.
:attr:`show_hidden_files` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
sort_by = OptionProperty(
"name", options=["nothing", "name", "date", "size", "type"]
)
"""
It can take the values 'nothing' 'name' 'date' 'size' 'type' - sorts files by option
By default, sort by name.
Available options are: `'nothing'`, `'name'`, `'date'`, `'size'`, `'type'`.
:attr:`sort_by` is an :class:`~kivy.properties.OptionProperty`
and defaults to `name`.
"""
sort_by_desc = BooleanProperty(False)
"""
Sort by descending.
:attr:`sort_by_desc` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
multiselect = BooleanProperty(False)
"""
Determines whether the user is able to select multiple files or not.
:attr:`multiselect` is a :class:`~kivy.properties.BooleanProperty` and defaults to
False.
"""
selection = ListProperty([])
"""
Contains the list of files that are currently selected.
:attr:`selection` is a read-only :class:`~kivy.properties.ListProperty` and
defaults to [].
"""
_window_manager = None
_window_manager_open = False
def __init__(self, **kwargs):
super().__init__(**kwargs)
toolbar_label = self.ids.toolbar.children[1].children[0]
toolbar_label.font_style = "Subtitle1"
self.add_widget(
FloatButton(
callback=self.select_directory_on_press_button,
md_bg_color=self.theme_cls.primary_color,
icon=self.icon,
)
)
if self.preview:
self.ext = [".png", ".jpg", ".jpeg"]
def __sort_files(self, files):
def sort_by_name(files):
files.sort(key=locale.strxfrm)
files.sort(key=str.casefold)
return files
if self.sort_by == "name":
sorted_files = sort_by_name(files)
elif self.sort_by == "date":
_files = sort_by_name(files)
_sorted_files = [os.path.join(self.current_path, f) for f in _files]
_sorted_files.sort(key=os.path.getmtime, reverse=True)
sorted_files = [os.path.basename(f) for f in _sorted_files]
elif self.sort_by == "size":
_files = sort_by_name(files)
_sorted_files = [os.path.join(self.current_path, f) for f in _files]
_sorted_files.sort(key=os.path.getsize, reverse=True)
sorted_files = [os.path.basename(f) for f in _sorted_files]
elif self.sort_by == "type":
_files = sort_by_name(files)
sorted_files = sorted(
_files,
key=lambda f: (os.path.splitext(f)[1], os.path.splitext(f)[0]),
)
else:
sorted_files = files
if self.sort_by_desc:
sorted_files.reverse()
return sorted_files
def show(self, path):
"""Forms the body of a directory tree.
:param path:
The path to the directory that will be opened in the file manager.
"""
self.current_path = path
self.selection = []
dirs, files = self.get_content()
manager_list = []
if dirs == [] and files == []: # selected directory
pass
elif not dirs and not files: # directory is unavailable
return
if self.preview:
for name_dir in self.__sort_files(dirs):
manager_list.append(
{
"viewclass": "BodyManagerWithPreview",
"path": self.icon_folder,
"realpath": os.path.join(path),
"type": "folder",
"name": name_dir,
"events_callback": self.select_dir_or_file,
"height": dp(150),
"_selected": False,
}
)
for name_file in self.__sort_files(files):
if (
os.path.splitext(os.path.join(path, name_file))[1]
in self.ext
):
manager_list.append(
{
"viewclass": "BodyManagerWithPreview",
"path": os.path.join(path, name_file),
"name": name_file,
"type": "files",
"events_callback": self.select_dir_or_file,
"height": dp(150),
"_selected": False,
}
)
else:
for name in self.__sort_files(dirs):
_path = os.path.join(path, name)
access_string = self.get_access_string(_path)
if "r" not in access_string:
icon = "folder-lock"
else:
icon = "folder"
manager_list.append(
{
"viewclass": "BodyManager",
"path": _path,
"icon": icon,
"dir_or_file_name": name,
"events_callback": self.select_dir_or_file,
"_selected": False,
}
)
for name in self.__sort_files(files):
if self.ext and os.path.splitext(name)[1] not in self.ext:
continue
manager_list.append(
{
"viewclass": "BodyManager",
"path": name,
"icon": "file-outline",
"dir_or_file_name": os.path.split(name)[1],
"events_callback": self.select_dir_or_file,
"_selected": False,
}
)
self.ids.rv.data = manager_list
if not self._window_manager:
self._window_manager = ModalView(
size_hint=(1, 1), auto_dismiss=False
)
self._window_manager.add_widget(self)
if not self._window_manager_open:
self._window_manager.open()
self._window_manager_open = True
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):
"""Returns a list of the type [[Folder List], [file list]]."""
try:
files = []
dirs = []
for content in os.listdir(self.current_path):
if os.path.isdir(os.path.join(self.current_path, content)):
if self.search == "all" or self.search == "dirs":
if (not self.show_hidden_files) and (
content.startswith(".")
):
continue
else:
dirs.append(content)
else:
if self.search == "all" or self.search == "files":
if len(self.ext) != 0:
try:
files.append(
os.path.join(self.current_path, content)
)
except IndexError:
pass
else:
if (
not self.show_hidden_files
and content.startswith(".")
):
continue
else:
files.append(content)
return dirs, files
except OSError:
return None, None
def close(self):
"""Closes the file manager window."""
self._window_manager.dismiss()
self._window_manager_open = False
def select_dir_or_file(self, path, widget):
"""Called by tap on the name of the directory or file."""
if os.path.isfile(os.path.join(self.current_path, path)):
if self.multiselect:
file_path = os.path.join(self.current_path, path)
if file_path in self.selection:
widget._selected = False
self.selection.remove(file_path)
else:
widget._selected = True
self.selection.append(file_path)
else:
self.select_path(os.path.join(self.current_path, path))
else:
self.current_path = path
self.show(path)
def back(self):
"""Returning to the branch down in the directory tree."""
path, end = os.path.split(self.current_path)
if not end:
self.close()
self.exit_manager(1)
else:
self.show(path)
def select_directory_on_press_button(self, *args):
"""Called when a click on a floating button."""
if len(self.selection) > 0:
self.select_path(self.selection)
else:
self.select_path(self.current_path)
Builder.load_string(ACTIVITY_MANAGER)