""" 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 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: size_hint_y: None height: self.texture_size[1] shorten: True shorten_from: "center" halign: "center" text_size: self.width, None 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 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 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" 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)