Sideband/sbapp/kivymd/uix/filemanager/filemanager.py
2023-07-10 02:49:58 +02:00

821 lines
25 KiB
Python
Executable File

"""
Components/FileManager
======================
A simple manager for selecting directories and files.
Usage
-----
.. code-block:: python
path = os.path.expanduser("~") # 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
.. warning:: Be careful! To use the `'/'` path on Android devices, you need
special permissions. Therefore, you are likely to get an error.
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-preview.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
import os
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 = '''
MDBoxLayout:
orientation: "vertical"
MDTopAppBar:
title: "MDFileManager"
left_action_items: [["menu", lambda x: None]]
elevation: 3
MDFloatLayout:
MDRoundFlatIconButton:
text: "Open manager"
icon: "folder"
pos_hint: {"center_x": .5, "center_y": .5}
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
)
def build(self):
self.theme_cls.theme_style = "Dark"
self.theme_cls.primary_palette = "Orange"
return Builder.load_string(KV)
def file_manager_open(self):
self.file_manager.show(os.path.expanduser("~")) # output manager to the screen
self.manager_open = True
def select_path(self, path: str):
'''
It will be called when you click on the file name
or the catalog selection button.
: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()
.. versionadded:: 1.0.0
Added a feature that allows you to show the available disks first, then the
files contained in them. Works correctly on: `Windows`, `Linux`, `OSX`, `Android`.
Not tested on `iOS`.
.. code-block:: python
def file_manager_open(self):
self.file_manager.show_disks()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/file-manager-show-disks.png
:align: center
"""
__all__ = ("MDFileManager",)
import locale
import os
import re
from typing import List, Tuple, Union
from kivy import platform
from kivy.clock import Clock
from kivy.factory import Factory
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import (
BooleanProperty,
ColorProperty,
ListProperty,
NumericProperty,
ObjectProperty,
OptionProperty,
StringProperty,
)
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.modalview import ModalView
from kivymd import images_path, uix_path
from kivymd.uix.behaviors import CircularRippleBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.button import MDFloatingActionButton
from kivymd.uix.fitimage import FitImage
from kivymd.uix.list import BaseListItem
from kivymd.uix.relativelayout import MDRelativeLayout
with open(
os.path.join(uix_path, "filemanager", "filemanager.kv"), encoding="utf-8"
) as kv_file:
Builder.load_string(kv_file.read())
class BodyManager(MDBoxLayout):
"""Base class for folders and files icons."""
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 ModifiedOneLineIconListItem(BaseListItem):
_txt_left_pad = NumericProperty("72dp")
_txt_top_pad = NumericProperty("16dp")
_txt_bot_pad = NumericProperty("15dp")
_num_lines = 1
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.height = dp(48)
class MDFileManager(MDRelativeLayout):
"""
Implements a modal dialog with a file manager.
For more information, see in the
:class:`~kivymd.uix.relativelayout.MDRelativeLayout` class documentation.
:Events:
`on_pre_open`:
Called before the MDFileManager is opened.
`on_open`:
Called when the MDFileManager is opened.
`on_pre_dismiss`:
Called before the MDFileManager is closed.
`on_dismiss`:
Called when the MDFileManager is closed.
"""
icon = StringProperty("check", deprecated=True)
"""
Icon that will be used on the directory selection button.
.. deprecated:: 1.1.0
Use :attr:`icon_selection_button` instead.
:attr:`icon` is an :class:`~kivy.properties.StringProperty`
and defaults to `check`.
"""
icon_selection_button = StringProperty("check")
"""
Icon that will be used on the directory selection button.
.. versionadded:: 1.1.0
.. code-block:: python
MDFileManager(
...
icon_selection_button="pencil",
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/file-manager-icon-selection-button.png
:align: center
:attr:`icon_selection_button` is an :class:`~kivy.properties.StringProperty`
and defaults to `check`.
"""
background_color_selection_button = ColorProperty(None)
"""
Background color in (r, g, b, a) or string format of the current
directory/path selection button.
.. versionadded:: 1.1.0
.. code-block:: python
MDFileManager(
...
background_color_selection_button="brown",
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/file-manager-background-color-selection-button.png
:align: center
:attr:`background_color_selection_button` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
background_color_toolbar = ColorProperty(None)
"""
Background color in (r, g, b, a) or string format of the file manager toolbar.
.. versionadded:: 1.1.0
.. code-block:: python
MDFileManager(
...
background_color_toolbar="brown",
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/file-manager-background-color-toolbar.png
:align: center
:attr:`background_color_toolbar` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
icon_folder = StringProperty(f"{images_path}folder.png")
"""
Icon that will be used for folder icons when using ``preview = True``.
.. code-block:: python
MDFileManager(
...
preview=True,
icon_folder="path/to/icon.png",
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/file-manager-icon-folder.png
:align: center
:attr:`icon` is an :class:`~kivy.properties.StringProperty`
and defaults to `check`.
"""
icon_color = ColorProperty(None)
"""
Color in (r, g, b, a) or string format of the folder icon when the
:attr:`preview` property is set to False.
.. versionadded:: 1.1.0
.. code-block:: python
MDFileManager(
...
preview=False,
icon_color="brown",
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/file-manager-icon-color.png
:align: center
:attr:`icon_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
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.path.expanduser("~"))
"""
Current directory.
:attr:`current_path` is an :class:`~kivy.properties.StringProperty`
and defaults to `os.path.expanduser("~")`.
"""
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`.
"""
selector = OptionProperty("any", options=["any", "file", "folder", "multi"])
"""
It can take the values 'any' 'file' 'folder' 'multi'
By default, any.
Available options are: `'any'`, `'file'`, `'folder'`, `'multi'`.
:attr:`selector` is an :class:`~kivy.properties.OptionProperty`
and defaults to `any`.
"""
selection = ListProperty()
"""
Contains the list of files that are currently selected.
:attr:`selection` is a read-only :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
selection_button = ObjectProperty()
"""
The instance of the directory/path selection button.
.. versionadded:: 1.1.0
:attr:`selection_button` is a read-only :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
_window_manager = None
_window_manager_open = False
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.register_event_type("on_pre_open")
self.register_event_type("on_open")
self.register_event_type("on_pre_dismiss")
self.register_event_type("on_dismiss")
toolbar_label = self.ids.toolbar.children[1].children[0]
toolbar_label.font_style = "Subtitle1"
Clock.schedule_once(self._create_selection_button)
if self.preview:
self.ext = [".png", ".jpg", ".jpeg"]
self.disks = []
def show_disks(self) -> None:
if platform == "win":
self.disks = sorted(
re.findall(
r"[A-Z]+:.*$",
os.popen("mountvol /").read(),
re.MULTILINE,
)
)
elif platform in ["linux", "android"]:
self.disks = sorted(
re.findall(
r"on\s(/.*)\stype",
os.popen("mount").read(),
)
)
elif platform == "macosx":
self.disks = sorted(
re.findall(
r"on\s(/.*)\s\(",
os.popen("mount").read(),
)
)
else:
return
self.current_path = ""
manager_list = []
for disk in self.disks:
access_string = self.get_access_string(disk)
if "r" not in access_string:
icon = "harddisk-remove"
else:
icon = "harddisk"
manager_list.append(
{
"viewclass": "BodyManager",
"path": disk,
"icon": icon,
"dir_or_file_name": disk,
"events_callback": self.select_dir_or_file,
"_selected": False,
}
)
self.ids.rv.data = manager_list
self._show()
def show(self, path: str) -> None:
"""
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,
"icon_color": self.theme_cls.primary_color
if not self.icon_color
else self.icon_color,
"_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,
"icon_color": self.theme_cls.primary_color
if not self.icon_color
else self.icon_color,
"_selected": False,
}
)
self.ids.rv.data = manager_list
self._show()
def get_access_string(self, path: str) -> str:
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,
) -> Union[Tuple[List[str], List[str]], Tuple[None, None]]:
"""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) -> None:
"""Closes the file manager window."""
self.dispatch("on_pre_dismiss")
self._window_manager.dismiss()
self.dispatch("on_dismiss")
self._window_manager_open = False
def select_dir_or_file(
self,
path: str,
widget: Union[BodyManagerWithPreview, Factory.BodyManager],
):
"""Called by tap on the name of the directory or file."""
if os.path.isfile(os.path.join(self.current_path, path)):
if self.selector == "multi":
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)
elif self.selector == "folder":
return
else:
self.select_path(os.path.join(self.current_path, path))
else:
self.current_path = path
self.show(path)
def back(self) -> None:
"""Returning to the branch down in the directory tree."""
path, end = os.path.split(self.current_path)
if self.current_path and path == self.current_path:
self.show_disks()
else:
if not end:
self.close()
self.exit_manager(1)
else:
self.show(path)
def select_directory_on_press_button(self, *args) -> None:
"""Called when a click on a floating button."""
if self.selector == "multi":
if len(self.selection) > 0:
self.select_path(self.selection)
else:
if self.selector == "folder" or self.selector == "any":
self.select_path(self.current_path)
def on_icon(self, instance_file_manager, icon_name: str) -> None:
"""Called when the :attr:`icon` property is changed."""
self.icon_selection_button = icon_name
def on_background_color_toolbar(
self, instance_file_manager, color: Union[str, list]
) -> None:
"""
Called when the :attr:`background_color_toolbar` property is changed.
"""
def on_background_color_toolbar(*args):
self.ids.toolbar.md_bg_color = color
Clock.schedule_once(on_background_color_toolbar)
def on_pre_open(self, *args):
"""
Default pre-open event handler.
.. versionadded:: 1.1.0
"""
def on_open(self, *args):
"""
Default open event handler.
.. versionadded:: 1.1.0
"""
def on_pre_dismiss(self, *args):
"""
Default pre-dismiss event handler.
.. versionadded:: 1.1.0
"""
def on_dismiss(self, *args):
"""
Default dismiss event handler.
.. versionadded:: 1.1.0
"""
def _show(self):
if not self._window_manager:
self._window_manager = ModalView(
size_hint=self.size_hint, auto_dismiss=False
)
self.size_hint = (1, 1)
self._window_manager.add_widget(self)
if not self._window_manager_open:
self._window_manager.open()
self._window_manager_open = True
self.dispatch("on_pre_open")
self.dispatch("on_open")
def _create_selection_button(self, *args):
if (
self.selector == "any"
or self.selector == "multi"
or self.selector == "folder"
):
self.selection_button = MDFloatingActionButton(
on_release=self.select_directory_on_press_button,
md_bg_color=self.theme_cls.primary_color
if not self.background_color_selection_button
else self.background_color_selection_button,
icon=self.icon_selection_button,
pos_hint={"right": 0.99},
y=dp(12),
elevation=0,
)
self.add_widget(self.selection_button)
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