├── docs ├── sources │ ├── _extensions │ │ ├── __init__.py │ │ ├── kivy_lexer.py │ │ ├── toctree_with_sort.py │ │ └── autoapi_filemanager.py │ ├── .gitignore │ ├── _static │ │ └── logo-kivymd.png │ ├── about.rst │ ├── index.rst │ ├── conf.py │ └── _templates │ │ └── python │ │ └── module.rst ├── Makefile └── make.bat ├── kivymd_extensions ├── filemanager │ ├── libs │ │ ├── __init__.py │ │ ├── plugins │ │ │ ├── contextmenu │ │ │ │ ├── remove │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── remove.kv │ │ │ │ ├── rename │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── rename.kv │ │ │ │ ├── ziparchive │ │ │ │ │ ├── dialog_zip.kv │ │ │ │ │ └── __init__.py │ │ │ │ ├── __init__.py │ │ │ │ └── properties │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── dialog_properties.kv │ │ │ └── __init__.py │ │ └── tools.py │ ├── data │ │ ├── settings.ini │ │ ├── header_menu.json │ │ ├── images │ │ │ ├── bg-field.png │ │ │ ├── closed.png │ │ │ ├── opened.png │ │ │ └── bg-field-dark.png │ │ ├── context-menu-items.json │ │ └── default_files_type.json │ ├── __init__.py │ ├── custom_splitter.kv │ ├── file_chooser_list.kv │ ├── file_chooser_icon.py │ ├── filemanager.kv │ └── filemanager.py └── __init__.py ├── .idea ├── modules.xml ├── filemanager.iml └── vcs.xml ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── pyproject.toml ├── .gitignore ├── LICENSE ├── examples └── full_example │ └── main.py ├── setup.py ├── .github └── workflows │ └── build.yml ├── setup.cfg └── README.md /docs/sources/_extensions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/libs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/sources/.gitignore: -------------------------------------------------------------------------------- 1 | # Directories and files that cannot be commited to git 2 | 3 | /api 4 | -------------------------------------------------------------------------------- /docs/sources/_static/logo-kivymd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kivymd-extensions/filemanager/HEAD/docs/sources/_static/logo-kivymd.png -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/data/settings.ini: -------------------------------------------------------------------------------- 1 | [General] 2 | palette = Blue 3 | tooltip = 1 4 | theme = Dark 5 | memorize_palette = 0 6 | 7 | -------------------------------------------------------------------------------- /kivymd_extensions/__init__.py: -------------------------------------------------------------------------------- 1 | import kivy 2 | 3 | kivy.require("2.0.0") 4 | __path__ = __import__("pkgutil").extend_path(__path__, __name__) 5 | -------------------------------------------------------------------------------- /docs/sources/_extensions/kivy_lexer.py: -------------------------------------------------------------------------------- 1 | from kivy.extras.highlight import KivyLexer 2 | 3 | 4 | def setup(app): 5 | app.add_lexer("kv", KivyLexer) 6 | -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/data/header_menu.json: -------------------------------------------------------------------------------- 1 | [ 2 | "home", 3 | "content-copy", 4 | "content-paste", 5 | "trash-can-outline", 6 | "share-variant", 7 | ] -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/data/images/bg-field.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kivymd-extensions/filemanager/HEAD/kivymd_extensions/filemanager/data/images/bg-field.png -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/data/images/closed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kivymd-extensions/filemanager/HEAD/kivymd_extensions/filemanager/data/images/closed.png -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/data/images/opened.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kivymd-extensions/filemanager/HEAD/kivymd_extensions/filemanager/data/images/opened.png -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/data/images/bg-field-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kivymd-extensions/filemanager/HEAD/kivymd_extensions/filemanager/data/images/bg-field-dark.png -------------------------------------------------------------------------------- /docs/sources/about.rst: -------------------------------------------------------------------------------- 1 | About 2 | ===== 3 | 4 | License 5 | ------- 6 | 7 | Refer to `LICENSE `_. 8 | 9 | .. literalinclude:: ../../LICENSE 10 | :language: none 11 | :emphasize-lines: 1,4 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.5" 2 | 3 | from kivy.factory import Factory 4 | 5 | from kivymd_extensions.filemanager.libs import tools 6 | from kivymd_extensions.filemanager.filemanager import FileManager 7 | 8 | Factory.register( 9 | "CustomFileChooserIcon", 10 | module="kivymd_extensions.filemanager.file_chooser_icon", 11 | ) 12 | -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/data/context-menu-items.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"": "Open"}, 3 | {}, 4 | {"open-in-app": "Open in app >"}, 5 | {"trash-can-outline": "Move to Trash", "cls": "move_to_trash"}, 6 | {"rename-box": "Rename", "cls": "rename"}, 7 | {"zip-box-outline": "Create zip", "cls": "create_zip"}, 8 | {}, 9 | {"": "Properties", "cls": "show_properties"} 10 | ] 11 | -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/libs/plugins/contextmenu/remove/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from kivy.lang import Builder 4 | 5 | from kivymd_extensions.filemanager.libs.plugins import PluginBaseDialog 6 | 7 | with open( 8 | os.path.join(os.path.dirname(__file__), "remove.kv"), encoding="utf-8" 9 | ) as kv: 10 | Builder.load_string(kv.read()) 11 | 12 | 13 | class DialogMoveToTrash(PluginBaseDialog): 14 | pass 15 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # Pre-commit hooks. 2 | # python -m pip install pre-commit 3 | # pre-commit install 4 | # pre-commit run --all-files 5 | 6 | repos: 7 | 8 | # Format Python 9 | - repo: https://github.com/psf/black 10 | rev: 20.8b1 11 | hooks: 12 | - id: black 13 | 14 | # Sort imports 15 | - repo: https://github.com/timothycrosley/isort 16 | rev: 5.6.4 17 | hooks: 18 | - id: isort 19 | additional_dependencies: ["toml"] 20 | 21 | # Lint Python 22 | - repo: https://gitlab.com/pycqa/flake8 23 | rev: 3.8.4 24 | hooks: 25 | - id: flake8 26 | -------------------------------------------------------------------------------- /docs/sources/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to File Manager documentation! 2 | ====================================== 3 | 4 | .. autoapimodule:: kivymd_extensions 5 | :noindex: 6 | 7 | Contents 8 | -------- 9 | 10 | .. toctree:: 11 | :hidden: 12 | 13 | Welcome 14 | 15 | .. toctree:: 16 | :maxdepth: 1 17 | 18 | /components/index 19 | /about 20 | 21 | .. toctree:: 22 | :maxdepth: 1 23 | :titlesonly: 24 | 25 | API 26 | 27 | Indices and tables 28 | ------------------ 29 | 30 | * :ref:`genindex` 31 | * :ref:`modindex` 32 | * :ref:`search` 33 | -------------------------------------------------------------------------------- /.idea/filemanager.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 16 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = sources 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/sources/conf.py 11 | builder: dirhtml 12 | 13 | # Optionally build your docs in additional formats such as PDF and ePub 14 | formats: all 15 | 16 | # Optionally set the version of Python and requirements required to build your docs 17 | python: 18 | version: 3.7 19 | install: 20 | - method: pip 21 | path: . 22 | extra_requirements: 23 | - docs 24 | 25 | search: 26 | ranking: 27 | # Components are more important 28 | components/*: 2 29 | 30 | # Unincluded API isn't very important 31 | api/*: -2 32 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # Format Python 2 | [tool.black] 3 | line-length = 80 4 | target-version = ['py35', 'py36', 'py37', 'py38'] 5 | include = '(\.pyi?$|\.spec$)' 6 | exclude = ''' 7 | ( 8 | /( 9 | \.eggs 10 | | \.git 11 | | \.hg 12 | | \.mypy_cache 13 | | \.tox 14 | | \.venv 15 | | _build 16 | | buck-out 17 | | build 18 | | dist 19 | )/ 20 | | buildozer\.spec 21 | ) 22 | ''' 23 | 24 | # Sort imports 25 | # Settings to fit Black formatting 26 | # Taken from https://github.com/timothycrosley/isort/issues/694 27 | [tool.isort] 28 | line_length = 80 29 | include_trailing_comma = true 30 | multi_line_output = 3 31 | use_parentheses = true 32 | force_grid_wrap = 0 33 | ensure_newline_before_comments = true 34 | known_third_party = ["setuptools", "kivymd", "kivy", "requests", "PIL", "android", "jnius", "watchdog", "sphinx", "docutils", "autoapi", "unidecode"] 35 | -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/libs/plugins/contextmenu/rename/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from kivy.clock import Clock 4 | from kivy.lang import Builder 5 | 6 | from kivymd_extensions.filemanager.libs.plugins import PluginBaseDialog 7 | 8 | with open( 9 | os.path.join(os.path.dirname(__file__), "rename.kv"), encoding="utf-8" 10 | ) as kv: 11 | Builder.load_string(kv.read()) 12 | 13 | 14 | class DialogRename(PluginBaseDialog): 15 | def set_focus(self, interval): 16 | self.ids.field.focus = True 17 | 18 | def rename_file(self, new_file_name): 19 | os.rename( 20 | self.instance_context_menu.entry_object.path, 21 | os.path.join( 22 | os.path.dirname(self.instance_context_menu.entry_object.path), 23 | new_file_name, 24 | ), 25 | ) 26 | self.dismiss() 27 | 28 | def on_open(self): 29 | Clock.schedule_once(self.set_focus) 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Directories and files that cannot be commited to git 2 | 3 | # Byte-compiled 4 | __pycache__ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | lib 14 | lib64 15 | parts 16 | sdist 17 | var 18 | wheels 19 | pip-wheel-metadata 20 | share/python-wheels 21 | develop-eggs 22 | eggs 23 | .eggs 24 | *.egg-info 25 | *.egg 26 | MANIFEST 27 | .installed.cfg 28 | downloads 29 | docs/_build 30 | build 31 | dist 32 | bin 33 | .buildozer 34 | 35 | # Logs 36 | *.log 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Editors 41 | .vscode 42 | .ipynb_checkpoints 43 | *.swp 44 | # PyCharm 45 | .idea/* 46 | !/.idea/*.iml 47 | !/.idea/modules.xml 48 | !/.idea/vcs.xml 49 | !/.idea/runConfigurations 50 | 51 | # Environments 52 | venv 53 | .venv 54 | env 55 | .env 56 | .python-version 57 | 58 | # Temp / Cache 59 | cache 60 | .cache 61 | temp 62 | .temp 63 | .pytest_cache 64 | .coverage 65 | -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/data/default_files_type.json: -------------------------------------------------------------------------------- 1 | { 2 | "pdf": "file-pdf-outline", 3 | "png": "file-image-outline", 4 | "gif": "file-image-outline", 5 | "jpg": "file-image-outline", 6 | "jpeg": "file-image-outline", 7 | "bmp": "file-image-outline", 8 | "zip": "zip-box-outline", 9 | "mp4": "file-video-outline", 10 | "3gp": "file-video-outline", 11 | "mpeg": "file-video-outline", 12 | "avi": "file-video-outline", 13 | "mov": "file-video-outline", 14 | "doc": "file-word-outline", 15 | "docx": "file-word-outline", 16 | "odt": "file-word-outline", 17 | "ini": "file-cog-outline", 18 | "xls": "file-exel-outline", 19 | "xml": "file-exel-outline", 20 | "xlam": "file-exel-outline", 21 | "mp3": "file-music-outline", 22 | "ogg": "file-music-outline", 23 | "wave": "file-music-outline", 24 | "py": "language-python", 25 | "pyc": "language-python", 26 | "js": "language-javascript", 27 | "java": "language-java", 28 | "cpp": "language-cpp", 29 | "c": "language-c", 30 | } -------------------------------------------------------------------------------- /docs/sources/_extensions/toctree_with_sort.py: -------------------------------------------------------------------------------- 1 | from docutils.parsers.rst import directives 2 | from sphinx.directives.other import TocTree, int_or_nothing 3 | 4 | 5 | class TocTreeWithSort(TocTree): 6 | option_spec = { 7 | "maxdepth": int, 8 | "name": directives.unchanged, 9 | "caption": directives.unchanged_required, 10 | "glob": directives.flag, 11 | "hidden": directives.flag, 12 | "includehidden": directives.flag, 13 | "numbered": int_or_nothing, 14 | "titlesonly": directives.flag, 15 | "reversed": directives.flag, 16 | "sorted": directives.flag, 17 | } 18 | 19 | def parse_content(self, toctree): 20 | ret = super().parse_content(toctree) 21 | if "sorted" in self.options: 22 | toctree["entries"] = sorted(toctree["entries"]) 23 | return ret 24 | 25 | 26 | def setup(app): 27 | directives.register_directive("toctree", TocTreeWithSort) 28 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=sources 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% -b dirhtml %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/custom_splitter.kv: -------------------------------------------------------------------------------- 1 | #:import Window kivy.core.window.Window 2 | #:import HoverBehavior kivymd.uix.behaviors.HoverBehavior 3 | 4 | 5 | 6 | 7 | 8 | <-SplitterStrip>: 9 | border: self.parent.border if self.parent else (3, 3, 3, 3) 10 | horizontal: "_h" if self.parent and self.parent.sizable_from[0] in ("t", "b") else "" 11 | background_normal: "atlas://data/images/defaulttheme/splitter{}{}".format("_disabled" if self.disabled else "", self.horizontal) 12 | background_down: "atlas://data/images/defaulttheme/splitter_down{}{}".format("_disabled" if self.disabled else "", self.horizontal) 13 | 14 | CustomStrip: 15 | pos: root.pos 16 | size: root.size 17 | allow_stretch: True 18 | source: 'atlas://data/images/defaulttheme/splitter_grip' + root.horizontal 19 | on_enter: Window.set_system_cursor("size_we") if root.parent.direction == "right-left" else Window.set_system_cursor("size_ns") 20 | on_leave: Window.set_system_cursor("arrow") 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 KivyMD Team and other contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/libs/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | from kivy.lang import Builder 2 | from kivy.properties import ListProperty, ObjectProperty 3 | from kivy.uix.modalview import ModalView 4 | 5 | from kivymd.theming import ThemableBehavior 6 | 7 | 8 | Builder.load_string( 9 | """ 10 | #:import images_path kivymd.images_path 11 | 12 | 13 | 14 | background: 15 | '{}/transparent.png'.format(images_path) \ 16 | if not root.bg_background else root.bg_background 17 | 18 | canvas: 19 | Color: 20 | rgba: 21 | root.theme_cls.bg_light if not root.bg_color else root.bg_color 22 | RoundedRectangle: 23 | pos: self.pos 24 | size: self.size 25 | radius: [15,] 26 | """ 27 | ) 28 | 29 | 30 | class PluginBaseDialog(ThemableBehavior, ModalView): 31 | """Base class for context menu windows.""" 32 | 33 | bg_color = ListProperty() # background color of the dialog 34 | bg_background = ListProperty() # path to background image of the dialog 35 | # 36 | instance_context_menu = ObjectProperty() 37 | # 38 | instance_manager = ObjectProperty() 39 | -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/libs/plugins/contextmenu/ziparchive/dialog_zip.kv: -------------------------------------------------------------------------------- 1 | 2 | size_hint: None, None 3 | height: container.height 4 | width: Window.width * 40 / 100 5 | auto_dismiss: False 6 | 7 | MDBoxLayout: 8 | id: container 9 | spacing: "12dp" 10 | padding: "12dp", "12dp", "12dp", "20dp" 11 | orientation: "vertical" 12 | adaptive_height: True 13 | 14 | MDBoxLayout: 15 | spacing: "20dp" 16 | adaptive_height: True 17 | 18 | MDIconButton: 19 | icon: "folder-zip" 20 | pos_hint: {'center_y': .5} 21 | user_font_size: "48sp" 22 | md_bg_color_disabled: 0, 0, 0, 0 23 | disabled: True 24 | 25 | MDLabel: 26 | text: "[b]Create archive:[/b]" 27 | font_style: "Subtitle1" 28 | pos_hint: {'center_y': .5} 29 | theme_text_color: "Custom" 30 | text_color: root.theme_cls.primary_color 31 | markup: True 32 | shorten: True 33 | 34 | MDLabel: 35 | id: lbl_name_file 36 | font_style: "Caption" 37 | 38 | MDProgressBar: 39 | id: progress_line 40 | max: 100 41 | -------------------------------------------------------------------------------- /examples/full_example/main.py: -------------------------------------------------------------------------------- 1 | from kivymd.app import MDApp 2 | 3 | from kivymd_extensions.filemanager import FileManager 4 | 5 | 6 | class Example(MDApp): 7 | def on_context_menu(self, instance_file_manager, name_context_plugin): 8 | print( 9 | "Event 'on_context_menu'", 10 | instance_file_manager, 11 | name_context_plugin, 12 | ) 13 | 14 | def on_tap_file(self, instance_file_manager, path): 15 | print("Event 'on_tap_file'", instance_file_manager, path) 16 | 17 | def on_tap_dir(self, instance_file_manager, path): 18 | print("Event 'on_tap_dir'", instance_file_manager, path) 19 | 20 | def on_tab_switch( 21 | self, 22 | instance_file_manager, 23 | instance_tabs, 24 | instance_tab, 25 | instance_tab_label, 26 | tab_text, 27 | ): 28 | print( 29 | "Event 'on_tab_switch'", 30 | instance_file_manager, 31 | instance_tabs, 32 | instance_tab, 33 | instance_tab_label, 34 | tab_text, 35 | ) 36 | 37 | def on_start(self): 38 | manager = FileManager() 39 | manager.bind( 40 | on_tap_file=self.on_tap_file, 41 | on_tap_dir=self.on_tap_dir, 42 | on_tab_switch=self.on_tab_switch, 43 | on_context_menu=self.on_context_menu, 44 | ) 45 | manager.open() 46 | 47 | 48 | Example().run() 49 | -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/libs/plugins/contextmenu/rename/rename.kv: -------------------------------------------------------------------------------- 1 | 2 | size_hint: None, None 3 | height: container.height 4 | width: Window.width * 40 / 100 5 | auto_dismiss: False 6 | 7 | MDBoxLayout: 8 | id: container 9 | spacing: "4dp" 10 | padding: "12dp", "12dp", "12dp", "8dp" 11 | orientation: "vertical" 12 | adaptive_height: True 13 | 14 | MDBoxLayout: 15 | adaptive_height: True 16 | 17 | MDIconButton: 18 | icon: "rename-box" 19 | pos_hint: {'center_y': .5} 20 | user_font_size: "48sp" 21 | md_bg_color_disabled: 0, 0, 0, 0 22 | disabled: True 23 | 24 | MDLabel: 25 | text: "[b]Rename:[/b]" 26 | font_style: "Subtitle1" 27 | pos_hint: {'center_y': .5} 28 | theme_text_color: "Custom" 29 | text_color: root.theme_cls.primary_color 30 | markup: True 31 | shorten: True 32 | 33 | MDSeparator: 34 | 35 | MDTextField: 36 | id: field 37 | text: os.path.split(root.instance_context_menu.entry_object.path)[1] 38 | 39 | MDBoxLayout: 40 | id: box 41 | adaptive_height: True 42 | spacing: "12dp" 43 | 44 | Widget: 45 | 46 | MDFlatButton: 47 | text: "CANCEL" 48 | on_release: root.dismiss() 49 | 50 | MDRaisedButton: 51 | text: "RENAME" 52 | on_release: root.rename_file(field.text) 53 | -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/libs/plugins/contextmenu/remove/remove.kv: -------------------------------------------------------------------------------- 1 | #:import os os 2 | #:import get_hex_from_color kivy.utils.get_hex_from_color 3 | 4 | 5 | 6 | size_hint: None, None 7 | height: container.height 8 | width: Window.width * 40 / 100 9 | auto_dismiss: False 10 | 11 | MDBoxLayout: 12 | id: container 13 | spacing: "8dp" 14 | padding: "12dp", "12dp", "12dp", "8dp" 15 | orientation: "vertical" 16 | adaptive_height: True 17 | 18 | MDBoxLayout: 19 | adaptive_height: True 20 | 21 | MDIconButton: 22 | icon: "trash-can" 23 | pos_hint: {'center_y': .5} 24 | user_font_size: "48sp" 25 | md_bg_color_disabled: 0, 0, 0, 0 26 | disabled: True 27 | 28 | MDLabel: 29 | text: 30 | f"[b]Remove " \ 31 | f"[color={get_hex_from_color(root.theme_cls.primary_color)}] " \ 32 | f"{os.path.split(root.instance_context_menu.entry_object.path)[1]}" \ 33 | f"[/color][/b]" 34 | font_style: "Subtitle1" 35 | pos_hint: {'center_y': .5} 36 | markup: True 37 | shorten: True 38 | 39 | MDSeparator: 40 | 41 | MDBoxLayout: 42 | adaptive_height: True 43 | spacing: "12dp" 44 | 45 | Widget: 46 | 47 | MDFlatButton: 48 | text: "CANCEL" 49 | on_release: root.dismiss() 50 | 51 | MDRaisedButton: 52 | text: "REMOVE" 53 | on_release: 54 | os.remove(root.instance_context_menu.entry_object.path) 55 | root.dismiss() 56 | -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/libs/plugins/contextmenu/ziparchive/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import zipfile 3 | 4 | from kivy.lang import Builder 5 | from kivymd.utils import asynckivy 6 | 7 | from kivymd_extensions.filemanager.libs.plugins import PluginBaseDialog 8 | 9 | with open( 10 | os.path.join(os.path.dirname(__file__), "dialog_zip.kv"), encoding="utf-8" 11 | ) as kv: 12 | Builder.load_string(kv.read()) 13 | 14 | 15 | class DialogZipArchive(PluginBaseDialog): 16 | def set_progress_value(self, count_files, count): 17 | self.ids.progress_line.value = int(count / count_files * 100) 18 | 19 | def set_name_packed_file(self, file_name): 20 | self.ids.lbl_name_file.text = file_name 21 | 22 | def on_open(self): 23 | async def create_zip(): 24 | if os.path.isdir(path): 25 | root_dir = os.path.basename(path) 26 | count = 0 27 | for dir_path, dir_names, file_names in os.walk(path): 28 | await asynckivy.sleep(0) 29 | for file_name in file_names: 30 | count += 1 31 | self.set_progress_value(count_files, count) 32 | self.set_name_packed_file(file_name) 33 | file_path = os.path.join(dir_path, file_name) 34 | parentpath = os.path.relpath(file_path, path) 35 | archive_name = os.path.join(root_dir, parentpath) 36 | zip_file.write(file_path, archive_name) 37 | else: 38 | zip_file.write(path, os.path.split(path)[1]) 39 | 40 | zip_file.close() 41 | self.dismiss() 42 | 43 | path = self.instance_context_menu.entry_object.path 44 | count_files = sum((len(f) for _, _, f in os.walk(path))) 45 | zip_file = zipfile.ZipFile(f"{path}.zip", "w", zipfile.ZIP_DEFLATED) 46 | asynckivy.start(create_zip()) 47 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from setuptools import find_packages, setup 5 | 6 | extension_name = "filemanager" 7 | package_name = "kivymd_extensions." + extension_name 8 | 9 | 10 | def get_version() -> str: 11 | """Get __version__ from __init__.py file.""" 12 | version_file = os.path.join( 13 | os.path.dirname(__file__), 14 | "kivymd_extensions", 15 | extension_name, 16 | "__init__.py", 17 | ) 18 | version_file_data = open(version_file, "rt", encoding="utf-8").read() 19 | version_regex = r"(?<=^__version__ = ['\"])[^'\"]+(?=['\"]$)" 20 | try: 21 | version = re.findall(version_regex, version_file_data, re.M)[0] 22 | return version 23 | except IndexError: 24 | raise ValueError(f"Unable to find version string in {version_file}.") 25 | 26 | 27 | if __name__ == "__main__": 28 | # Static strings are in setup.cfg 29 | setup( 30 | name=package_name, 31 | description="File manager for desktop", 32 | version=get_version(), 33 | packages=( 34 | ["kivymd_extensions"] 35 | + find_packages(include=[package_name, package_name + ".*"]) 36 | ), 37 | package_dir={package_name: package_name.replace(".", os.sep)}, 38 | package_data={package_name: ["*.kv", "data/*", "data/images/*.png"]}, 39 | extras_require={ 40 | "dev": [ 41 | "pre-commit", 42 | "black", 43 | "isort[pyproject]", 44 | "flake8", 45 | "pytest", 46 | "pytest-cov", 47 | "pytest_asyncio", 48 | "pytest-timeout", 49 | "coveralls", 50 | ], 51 | "docs": [ 52 | "sphinx", 53 | "sphinx-autoapi==1.4.0", 54 | "sphinx_rtd_theme", 55 | ], 56 | }, 57 | install_requires=["kivymd>=0.104.1", "kivy>=1.11.1"], 58 | setup_requires=[], 59 | python_requires=">=3.6", 60 | ) 61 | -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/libs/plugins/contextmenu/__init__.py: -------------------------------------------------------------------------------- 1 | from .properties import DialogProperties 2 | from .remove import DialogMoveToTrash 3 | from .rename import DialogRename 4 | from .ziparchive import DialogZipArchive 5 | 6 | 7 | class ContextMenuPlugin: 8 | def __init__(self, instance_manager=None, entry_object=None): 9 | # <__main__.FileManager object> 10 | self.instance_manager = instance_manager 11 | # 12 | self.entry_object = entry_object 13 | 14 | self.plugin_dialogs = { 15 | "rename": DialogRename, 16 | "move_to_trash": DialogMoveToTrash, 17 | "create_zip": DialogZipArchive, 18 | "show_properties": DialogProperties, 19 | } 20 | 21 | def dismiss_plugin_dialog(self, instance_plugin_dialog, name_plugin): 22 | if ( 23 | name_plugin in self.plugin_dialogs.keys() 24 | and name_plugin != "show_properties" 25 | ): 26 | self.instance_manager.update_files( 27 | instance_plugin_dialog, self.entry_object.path 28 | ) 29 | self.instance_manager.dispatch("on_context_menu", name_plugin) 30 | self.instance_manager.dispatch( 31 | "on_dismiss_plugin_dialog", self.instance_manager, self 32 | ) 33 | 34 | def main(self, name_plugin): 35 | if name_plugin in self.plugin_dialogs.keys(): 36 | self.plugin_dialogs[name_plugin]( 37 | instance_context_menu=self, 38 | instance_manager=self.instance_manager, 39 | size_hint_x=0.6, 40 | on_dismiss=lambda x, name_plugin=name_plugin: self.dismiss_plugin_dialog( 41 | x, name_plugin 42 | ), 43 | on_open=self._on_open_plugin_dialog, 44 | ).open() 45 | 46 | def _on_open_plugin_dialog(self, *args): 47 | self.instance_manager.dispatch( 48 | "on_open_plugin_dialog", self.instance_manager, self 49 | ) 50 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | pull_request: 5 | 6 | jobs: 7 | 8 | # Build job. Builds source distribution (sdist) and binary wheels distribution (bdist_wheel) 9 | build: 10 | name: Build ${{ matrix.dist }} [${{ matrix.python-version }} | ${{ matrix.os }} ${{ matrix.architecture }}] 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | include: 15 | - os: ubuntu-latest # Modify one element to build source distribution 16 | python-version: 3.7 17 | architecture: x64 18 | dist: sdist bdist_wheel 19 | runs-on: ${{ matrix.os }} 20 | env: 21 | PYTHONUNBUFFERED: 1 22 | 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v2 26 | 27 | - name: Set up Python ${{ matrix.python-version }} 28 | uses: actions/setup-python@v2 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | architecture: ${{ matrix.architecture }} 32 | 33 | - name: Build 34 | run: | 35 | python -m pip install --upgrade pip 36 | python -m pip install --upgrade setuptools wheel 37 | python setup.py ${{ matrix.dist }} 38 | 39 | - name: Upload artifacts 40 | uses: actions/upload-artifact@v2 41 | with: 42 | name: dist 43 | path: dist 44 | 45 | # Deploy job. Uploads distribution to PyPI (only on tags) 46 | deploy: 47 | name: Publish to PyPI 48 | runs-on: ubuntu-latest 49 | needs: build 50 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') 51 | env: 52 | PYTHONUNBUFFERED: 1 53 | 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@v2 57 | 58 | - name: Download artifacts 59 | uses: actions/download-artifact@v2 60 | with: 61 | name: dist 62 | path: dist 63 | 64 | - name: Publish to PyPI 65 | uses: pypa/gh-action-pypi-publish@v1.2.2 66 | with: 67 | user: __token__ 68 | password: ${{ secrets.PYPI_TOKEN }} 69 | packages_dir: dist/ 70 | -------------------------------------------------------------------------------- /docs/sources/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 3 | 4 | # Path setup 5 | import os 6 | import sys 7 | 8 | sys.path.insert(0, os.path.abspath("_extensions")) 9 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(".")))) 10 | 11 | import autoapi_filemanager # NOQA. from _extensions 12 | 13 | # Project information 14 | project = "File Manager" 15 | copyright = "2020 KivyMD Team" 16 | author = "KivyMD Team" 17 | version = "0.1.0" 18 | release = "0.1.0" 19 | 20 | 21 | # General configuration 22 | master_doc = "index" 23 | exclude_patterns = [] 24 | templates_path = ["_templates"] 25 | locale_dirs = ["_locales"] 26 | language = "Python" 27 | 28 | 29 | # HTML Theme 30 | html_theme = "sphinx_rtd_theme" 31 | html_static_path = ["_static"] 32 | html_favicon = "_static/logo-kivymd.png" 33 | html_logo = "_static/logo-kivymd.png" 34 | html_theme_options = { 35 | "canonical_url": "https://kivymd.readthedocs.io/en/latest/", 36 | "navigation_depth": 2, 37 | "collapse_navigation": False, 38 | "titles_only": True, 39 | } 40 | 41 | 42 | # Extensions 43 | extensions = [ 44 | "sphinx.ext.autodoc", 45 | "autoapi_filemanager", 46 | "sphinx.ext.intersphinx", 47 | "kivy_lexer", 48 | "toctree_with_sort", 49 | ] 50 | 51 | # AutoAPI configuration 52 | autoapi_dirs = ["../../kivymd_extensions/filemanager"] 53 | autoapi_template_dir = os.path.abspath("_templates") 54 | autoapi_type = "python" 55 | autoapi_file_patterns = ["*.py"] 56 | autoapi_generate_api_docs = True 57 | autoapi_options = ["members", "undoc-members"] 58 | autoapi_root = "api" 59 | autoapi_add_toctree_entry = False 60 | autoapi_include_inheritance_graphs = False 61 | autoapi_include_summaries = True 62 | autoapi_python_class_content = "class" 63 | autoapi_python_use_implicit_namespaces = False 64 | autoapi_keep_files = False # True for debugging 65 | 66 | # InterSphinx configuration 67 | intersphinx_mapping = { 68 | "python": ("https://docs.python.org/3", None), 69 | "kivy": ("https://kivy.org/doc/stable/", None), 70 | } 71 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | author = KivyMD Team and other contributors 3 | author_email = kivydevelopment@gmail.com 4 | long_description = file: README.md 5 | long_description_content_type = text/markdown; charset=UTF-8 6 | keywords = kivymd_extensions, kivymd, kivy, material, ui 7 | license = MIT 8 | license_file = LICENSE 9 | url = https://github.com/kivymd-extensions/filemanager 10 | project_urls = 11 | Tracker=https://github.com/kivymd-extensions/filemanager/issues 12 | classifiers = Development Status :: 3 - Alpha 13 | License :: OSI Approved :: MIT License 14 | Programming Language :: Python :: 3 :: Only 15 | Programming Language :: Python :: 3 16 | Programming Language :: Python :: 3.6 17 | Programming Language :: Python :: 3.7 18 | Programming Language :: Python :: 3.8 19 | Programming Language :: Python :: 3.9 20 | Operating System :: OS Independent 21 | Operating System :: Android 22 | Operating System :: POSIX :: Linux 23 | Operating System :: POSIX :: BSD :: FreeBSD 24 | Operating System :: Microsoft :: Windows 25 | Operating System :: iOS 26 | Operating System :: MacOS 27 | Operating System :: MacOS :: MacOS X 28 | Environment :: MacOS X 29 | Environment :: Win32 (MS Windows) 30 | Environment :: X11 Applications 31 | Intended Audience :: Developers 32 | Intended Audience :: End Users/Desktop 33 | Intended Audience :: Information Technology 34 | Intended Audience :: Science/Research 35 | Topic :: Software Development :: User Interfaces 36 | Topic :: Scientific/Engineering :: Human Machine Interfaces 37 | Topic :: Scientific/Engineering :: Visualization 38 | platforms = any 39 | 40 | [upload] 41 | repository = https://upload.pypi.org/legacy/ 42 | 43 | [flake8] 44 | ignore = E501,W503,E203,E731 45 | max-line-length = 80 46 | exclude = .git/,__pycache__,build/,.eggs/,.buildozer 47 | statistics = true 48 | count = true 49 | -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/libs/plugins/contextmenu/properties/__init__.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | from kivy.clock import Clock 5 | from kivy.lang import Builder 6 | 7 | from kivymd_extensions.filemanager.libs.plugins import PluginBaseDialog 8 | from kivymd_extensions.filemanager.libs.tools import ( 9 | file_size, 10 | get_access_string, 11 | ) 12 | 13 | with open( 14 | os.path.join(os.path.dirname(__file__), "dialog_properties.kv"), 15 | encoding="utf-8", 16 | ) as kv: 17 | Builder.load_string(kv.read()) 18 | 19 | 20 | class DialogProperties(PluginBaseDialog): 21 | def get_first_created(self): 22 | return datetime.datetime.fromtimestamp( 23 | int(os.path.getctime(self.instance_context_menu.entry_object.path)) 24 | ) 25 | 26 | def get_last_opened(self): 27 | return datetime.datetime.fromtimestamp( 28 | int(os.path.getatime(self.instance_context_menu.entry_object.path)) 29 | ) 30 | 31 | def get_last_changed(self): 32 | return datetime.datetime.fromtimestamp( 33 | int(os.path.getmtime(self.instance_context_menu.entry_object.path)) 34 | ) 35 | 36 | def get_file_size(self): 37 | path = self.instance_context_menu.entry_object.path 38 | if os.path.isfile(path): 39 | return file_size(self.instance_context_menu.entry_object.path) 40 | else: 41 | count = 0 42 | for d, dirs, files in os.walk(path): 43 | count += len(files) 44 | return f"Count files {count}" 45 | 46 | def get_access_string(self): 47 | return get_access_string(self.instance_context_menu.entry_object.path) 48 | 49 | def set_access(self, interval): 50 | access_list = list(self.get_access_string()) 51 | access_data = { 52 | "Read": self.ids.r, 53 | "Write": self.ids.w, 54 | "Executable": self.ids.x, 55 | } 56 | for i, access_text in enumerate(access_data.keys()): 57 | access_data[ 58 | access_text 59 | ].text = f"[b]{access_text}:[/b] {'Yes' if access_list[i] != '-' else 'No'}" 60 | 61 | def on_instance_context_menu(self, instance, value): 62 | Clock.schedule_once(self.set_access) 63 | 64 | def on_pre_open(self): 65 | if os.path.isdir(self.instance_context_menu.entry_object.path): 66 | self.ids.container.remove_widget(self.ids.x) 67 | -------------------------------------------------------------------------------- /docs/sources/_templates/python/module.rst: -------------------------------------------------------------------------------- 1 | :github_url: https://github.com/kivymd/KivyMD/blob/master/{{ obj.obj.relative_path|replace("\\", "/") }} 2 | {% if not obj.display %} 3 | {# Do not display warnings #} 4 | :orphan: 5 | {% endif %} 6 | 7 | {# Write last word in summary #} 8 | {% set unincluded = obj.include_dir("").startswith("/api") %} 9 | {% set summary_split = obj.summary.split("/") %} 10 | {% set name = summary_split[-1] %} 11 | {% if name %} 12 | {{ name }} 13 | {{ "=" * name|length }} 14 | {% else %} 15 | {{ obj.name }} 16 | {{ "=" * obj.name|length }} 17 | {% endif %} 18 | 19 | .. py:module:: {{ obj.name }} 20 | 21 | {# Write docstring of module #} 22 | {% if obj.docstring %} 23 | .. autoapi-nested-parse:: 24 | 25 | {{ obj.docstring|prepare_docstring|indent(3) }} 26 | {% endif %} 27 | 28 | {% block api %} 29 | {# API. Write module name #} 30 | API - :mod:`{{ obj.name }}` 31 | {{ "-" * 13 }}{{ "-" * obj.name|length }} 32 | 33 | {% if obj.all is not none %} 34 | {# Get all visible children #} 35 | {% set visible_children = obj.children|selectattr("short_name", "in", obj.all)|list %} 36 | {% elif obj.type is equalto("package") %} 37 | {% set visible_children = obj.children|selectattr("display")|list %} 38 | {% else %} 39 | {% set visible_children = obj.children|selectattr("display")|rejectattr("imported")|list %} 40 | {% endif %} 41 | {% if visible_children %} 42 | {# Write all visible children #} 43 | {% for obj_item in visible_children %} 44 | {{ obj_item.rendered|indent(0) }} 45 | {% endfor %} 46 | {% endif %} 47 | {% endblock %} 48 | 49 | {# Write submodules and subpackages if it is package #} 50 | {% block submodules %} 51 | {% set visible_submodules = obj.submodules|selectattr("display")|list %} 52 | {% set visible_subpackages = obj.subpackages|selectattr("display")|list %} 53 | {% if visible_submodules or visible_subpackages %} 54 | Submodules 55 | ---------- 56 | {% if visible_submodules %} 57 | 58 | .. toctree:: 59 | :titlesonly: 60 | :maxdepth: 1 61 | 62 | {% for submodule in visible_submodules %} 63 | {% if unincluded == submodule.include_dir("").startswith("/api") %} 64 | {{ submodule.name }} <{{ submodule.include_dir("") }}/index> 65 | {% endif %} 66 | {% endfor %} 67 | {% endif %} 68 | {% if visible_subpackages %} 69 | 70 | .. toctree:: 71 | :titlesonly: 72 | :maxdepth: 3 73 | 74 | {% for subpackage in visible_subpackages %} 75 | {% if unincluded == subpackage.include_dir("").startswith("/api") %} 76 | {{ subpackage.name }} <{{ subpackage.include_dir("") }}/index> 77 | {% endif %} 78 | {% endfor %} 79 | {% endif %} 80 | {% endif %} 81 | {% endblock %} 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # File Manager 2 | 3 | 4 | 5 | Desktop file manager developed on the Kivy platform using the KivyMD library. 6 | Perhaps the file manager will work on mobile devices, but we are not even trying to check if this is the case. 7 | We are not testing this library on mobile devices or adapting it for mobile devices. 8 | Because, as the name suggests, we are developing this module for desktop use. 9 | 10 | [![Documentation Status](https://readthedocs.org/projects/file-manager/badge/?version=latest)](https://file-manager.readthedocs.io/en/latest/?badge=latest) 11 | 12 | ## Installation 13 | 14 | ```bash 15 | pip install kivymd_extensions.filemanager 16 | ``` 17 | 18 | ### Dependencies: 19 | 20 | - [KivyMD](https://github.com/kivymd/KivyMD) >= master version 21 | - [Kivy](https://github.com/kivy/kivy) >= 2.0.0 ([Installation](https://kivy.org/doc/stable/gettingstarted/installation.html)) 22 | - [Python 3.6+](https://www.python.org/) 23 | 24 | ## Documentation 25 | See documentation at https://file-manager.readthedocs.io/en/latest/components/file-manager/ 26 | 27 | ### Usage 28 | 29 | ```python 30 | from kivymd.app import MDApp 31 | 32 | from kivymd_extensions.filemanager import FileManager 33 | 34 | 35 | class MainApp(MDApp): 36 | def on_start(self): 37 | FileManager().open() 38 | 39 | 40 | if __name__ == "__main__": 41 | MainApp().run() 42 | ``` 43 | 44 |

45 | 46 | 47 | 48 |

49 | 50 | ### Customization 51 | 52 | ```python 53 | FileManager(path_to_skin="/Users/macbookair/data/images").open() 54 | ``` 55 | 56 |

57 | 58 | 59 | 60 |

61 | 62 | ## Examples 63 | 64 | ```bash 65 | git clone https://github.com/kivymd-extensions/filemanager.git 66 | cd filemanager 67 | cd examples/full_example 68 | python main.py 69 | ``` 70 | 71 | ### Support 72 | 73 | If you need assistance or you have a question, you can ask for help on our mailing list: 74 | 75 | - **Discord server:** https://discord.gg/wu3qBST 76 | - _Email:_ KivyMD-library@yandex.com 77 | -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/libs/tools.py: -------------------------------------------------------------------------------- 1 | import string 2 | import os 3 | 4 | from os import walk 5 | from os.path import expanduser, isdir, dirname, join, sep 6 | 7 | from kivy.utils import platform 8 | 9 | 10 | def convert_bytes(num): 11 | """Convert bytes to MB.... GB... etc.""" 12 | 13 | for x in ["bytes", "KB", "MB", "GB", "TB"]: 14 | if num < 1024.0: 15 | return "%3.1f %s" % (num, x) 16 | num /= 1024.0 17 | 18 | 19 | def file_size(file_path): 20 | """Return the file size.""" 21 | 22 | if os.path.isfile(file_path): 23 | file_info = os.stat(file_path) 24 | return convert_bytes(file_info.st_size) 25 | 26 | 27 | def get_access_string(path): 28 | """Return strind `rwx`.""" 29 | 30 | access_string = "" 31 | access_data = {"r": os.R_OK, "w": os.W_OK, "x": os.X_OK} 32 | for access in access_data.keys(): 33 | access_string += access if os.access(path, access_data[access]) else "-" 34 | return access_string 35 | 36 | 37 | def get_icon_for_treeview(path, ext, isdir): 38 | icon_image = "file" 39 | if isdir: 40 | access_string = get_access_string(path) 41 | if "r" not in access_string: 42 | icon_image = "folder-lock" 43 | else: 44 | icon_image = "folder" 45 | else: 46 | if ext == ".py": 47 | icon_image = "language-python" 48 | return icon_image 49 | 50 | 51 | def get_home_directory(): 52 | if platform == "win": 53 | user_path = expanduser("~") 54 | if not isdir(join(user_path, "Desktop")): 55 | user_path = dirname(user_path) 56 | else: 57 | user_path = expanduser("~") 58 | 59 | return user_path 60 | 61 | 62 | def get_drives(): 63 | drives = [] 64 | if platform == "win": 65 | from ctypes import windll, create_unicode_buffer 66 | 67 | bitmask = windll.kernel32.GetLogicalDrives() 68 | GetVolumeInformationW = windll.kernel32.GetVolumeInformationW 69 | 70 | for letter in string.ascii_uppercase: 71 | if bitmask & 1: 72 | name = create_unicode_buffer(64) 73 | # get name of the drive 74 | drive = letter + ":" 75 | res = GetVolumeInformationW( 76 | drive + sep, name, 64, None, None, None, None, 0 77 | ) 78 | if isdir(drive): 79 | drives.append((drive, name.value)) 80 | bitmask >>= 1 81 | elif platform == "linux": 82 | drives.append((sep, sep)) 83 | drives.append((expanduser("~"), "~/")) 84 | places = (sep + "mnt", sep + "media") 85 | for place in places: 86 | if isdir(place): 87 | for directory in next(walk(place))[1]: 88 | drives.append((place + sep + directory, directory)) 89 | elif platform == "macosx": 90 | drives.append((expanduser("~"), "~/")) 91 | vol = sep + "Volume" 92 | if isdir(vol): 93 | for drive in next(walk(vol))[1]: 94 | drives.append((vol + sep + drive, drive)) 95 | return drives 96 | -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/libs/plugins/contextmenu/properties/dialog_properties.kv: -------------------------------------------------------------------------------- 1 | #:import os os 2 | #:import STANDARD_INCREMENT kivymd.material_resources.STANDARD_INCREMENT 3 | 4 | 5 | 6 | shorten: True 7 | markup: True 8 | size_hint_y: None 9 | height: self.texture_size[1] 10 | 11 | 12 | 13 | size_hint_y: None 14 | height: container.height 15 | 16 | MDBoxLayout: 17 | id: container 18 | orientation: "vertical" 19 | adaptive_height: True 20 | spacing: "12dp" 21 | padding: "12dp" 22 | 23 | MDBoxLayout 24 | size_hint_y: None 25 | height: STANDARD_INCREMENT 26 | spacing: "12dp" 27 | 28 | MDIconButton: 29 | user_font_size: STANDARD_INCREMENT 30 | pos_hint: {"center_y": .5} 31 | theme_text_color: "Custom" 32 | md_bg_color_disabled: 0, 0, 0, 0 33 | text_color: root.theme_cls.primary_color 34 | disabled: True 35 | icon: 36 | root.instance_context_menu.instance_manager.get_icon_file(root.instance_context_menu.entry_object.path) \ 37 | if os.path.isfile((root.instance_context_menu.entry_object.path)) \ 38 | else ("folder" if not root.instance_manager.path_to_skin else os.path.join(root.instance_manager.path_to_skin, "folder")) 39 | 40 | MDBoxLayout: 41 | orientation: "vertical" 42 | 43 | MDBoxLayout: 44 | 45 | MDLabel: 46 | text: os.path.split(root.instance_context_menu.entry_object.path)[1] 47 | shorten: True 48 | font_style: "Caption" 49 | bold: True 50 | theme_text_color: "Custom" 51 | text_color: root.theme_cls.primary_color 52 | 53 | MDLabel: 54 | text: root.get_file_size() 55 | halign: "right" 56 | shorten: True 57 | font_style: "Caption" 58 | 59 | MDLabel: 60 | text: f"Modified {root.get_last_changed()}" 61 | shorten: True 62 | font_style: "Caption" 63 | 64 | MDSeparator: 65 | 66 | LabelDialogProperties: 67 | text: f"[b]Date of creation:[/b] {root.get_first_created()}" 68 | 69 | LabelDialogProperties: 70 | text: f"[b]Date last opened:[/b] {root.get_last_opened()}" 71 | 72 | Widget: 73 | size_hint_y: None 74 | height: "4dp" 75 | 76 | MDLabel: 77 | text: f"Access rights" 78 | font_style: "H6" 79 | theme_text_color: "Custom" 80 | text_color: root.theme_cls.primary_color 81 | 82 | Widget: 83 | size_hint_y: None 84 | height: "4dp" 85 | 86 | LabelDialogProperties: 87 | id: r 88 | 89 | LabelDialogProperties: 90 | id: w 91 | 92 | LabelDialogProperties: 93 | id: x -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/file_chooser_list.kv: -------------------------------------------------------------------------------- 1 | #:import os os 2 | #:import filemanager kivymd_extensions.filemanager 3 | 4 | 5 | <-FileChooserListLayout> 6 | on_entry_added: treeview.add_node(args[1]) 7 | on_entries_cleared: treeview.root.nodes = [] 8 | on_subentry_to_entry: not args[2].locked and treeview.add_node(args[1], args[2]) 9 | on_remove_subentry: args[2].nodes = [] 10 | 11 | BoxLayout: 12 | pos: root.pos 13 | size: root.size 14 | size_hint: None, None 15 | orientation: 'vertical' 16 | 17 | ScrollView: 18 | id: scrollview 19 | do_scroll_x: False 20 | 21 | Scatter: 22 | do_rotation: False 23 | do_scale: False 24 | do_translation: False 25 | size: treeview.size 26 | size_hint_y: None 27 | 28 | TreeView: 29 | id: treeview 30 | hide_root: True 31 | size_hint_y: None 32 | width: scrollview.width 33 | height: self.minimum_height 34 | on_node_expand: root.controller.entry_subselect(args[1]) 35 | on_node_collapse: root.controller.close_subselection(args[1]) 36 | 37 | 38 | <-TreeViewNode> 39 | canvas.before: 40 | Color: 41 | rgba: 42 | self.odd_color if not self.is_selected else app.theme_cls.bg_light \ 43 | if app.theme_cls.theme_style == "Dark" else app.theme_cls.bg_darkest 44 | Rectangle: 45 | pos: [self.parent.x, self.y] if self.parent else [0, 0] 46 | size: [self.parent.width, self.height] if self.parent else [1, 1] 47 | Color: 48 | rgba: 1, 1, 1, int(not self.is_leaf) 49 | Rectangle: 50 | source: 51 | os.path.join(os.path.dirname(os.path.dirname(filemanager.tools.__file__)), \ 52 | 'data', 'images', "{}.png".format('opened' if self.is_open else 'closed')) 53 | 54 | size: self.height / 2.5, self.height / 2.5 55 | pos: self.x - 10, self.center_y - 6 56 | canvas.after: 57 | Color: 58 | rgba: .5, .5, .5, .2 59 | 60 | 61 | <-FileChooserProgress> 62 | pos_hint: {'x': 0, 'y': 0} 63 | 64 | canvas: 65 | Color: 66 | rgba: 0, 0, 0, .3 67 | Rectangle: 68 | pos: self.pos 69 | size: self.size 70 | 71 | Label: 72 | pos_hint: {'center_x': .5, 'y': .6} 73 | size_hint: None, .2 74 | width: self.texture_size[0] 75 | text: 'Opening %s' % root.path 76 | halign: 'center' 77 | shorten: True 78 | shorten_from: 'center' 79 | text_size: root.width, None 80 | 81 | FloatLayout: 82 | pos_hint: {'x': .2, 'y': .4} 83 | size_hint: .6, .2 84 | 85 | MDProgressBar: 86 | id: pb 87 | pos_hint: {'x': 0, 'center_y': .5} 88 | max: root.total 89 | value: root.index 90 | 91 | Label: 92 | pos_hint: {'x': 0} 93 | text: '%d / %d' % (root.index, root.total) 94 | size_hint_y: None 95 | height: self.texture_size[1] 96 | y: pb.center_y - self.height - 8 97 | font_size: '13sp' 98 | color: (.8, .8, .8, .8) 99 | 100 | AnchorLayout: 101 | pos_hint: {'x': .2, 'y': .2} 102 | size_hint: .6, .2 103 | 104 | MDRaisedButton: 105 | text: 'Cancel' 106 | on_release: root.cancel() 107 | 108 | 109 | [FileListEntry@FloatLayout+TreeViewNode] 110 | locked: False 111 | entries: [] 112 | path: ctx.path 113 | is_selected: self.path in ctx.controller().selection 114 | size_hint_y: None 115 | height: '128dp' if dp(1) > 1 else '24dp' 116 | is_leaf: not ctx.isdir or ctx.name.endswith('..' + ctx.sep) or self.locked 117 | on_touch_down: 118 | if self.collide_point(*args[1].pos): ctx.controller().manager.tap_on_file_dir(args, "FileChooserList") 119 | if not ctx.controller().manager.context_menu_open: ctx.controller().callback(ctx.path) 120 | 121 | BoxLayout: 122 | orientation: 'vertical' 123 | x: root.pos[0] 124 | y: root.pos[1] - dp(15) 125 | size_hint_x: None 126 | width: root.width - dp(10) 127 | spacing: dp(10) 128 | padding: dp(10) 129 | 130 | MDBoxLayout: 131 | adaptive_height: True 132 | spacing: dp(10) 133 | 134 | MDIconButton: 135 | user_font_size: "15sp" 136 | theme_text_color: "Custom" 137 | text_color: app.theme_cls.primary_color 138 | _no_ripple_effect: True 139 | icon: 140 | filemanager.tools.get_icon_for_treeview(\ 141 | ctx.path, os.path.splitext(ctx.name)[1], ctx.isdir) 142 | 143 | Label: 144 | id: filename 145 | text_size: self.width, None 146 | halign: 'left' 147 | shorten: True 148 | text: ctx.name.split('#')[0] if '#' in ctx.name else ctx.name 149 | bold: True 150 | font_size: '12sp' 151 | color: app.theme_cls.text_color 152 | -------------------------------------------------------------------------------- /docs/sources/_extensions/autoapi_filemanager.py: -------------------------------------------------------------------------------- 1 | """ 2 | Monkey patching for AutoAPI Sphinx module. 3 | 4 | Arrange .rst files by their summaries. Write path in the first line of docstring 5 | to add .rst file. For example, "Classes/My Cool Class" will be placed in 6 | "/classes/my-cool-class/index.rst". 7 | 8 | It patches :func:`autoapi.mappers.python.objects.PythonPythonMapper.include_dir`, 9 | :func:`autoapi.mappers.python.objects.PythonPythonMapper.pathname`, 10 | :func:`autoapi.mappers.python.mapper.PythonSphinxMapper.output_rst`. 11 | """ 12 | 13 | import os 14 | import re 15 | 16 | import autoapi 17 | import sphinx 18 | import unidecode 19 | from autoapi.extension import LOGGER 20 | from autoapi.extension import setup as autoapi_setup 21 | from autoapi.mappers.python.mapper import PythonSphinxMapper 22 | from autoapi.mappers.python.objects import PythonPythonMapper 23 | from sphinx.util.console import bold, darkgreen 24 | from sphinx.util.osutil import ensuredir 25 | 26 | 27 | def PythonPythonMapper_include_dir(self: PythonPythonMapper, root): 28 | if os.path.isabs(root): 29 | parts = [self.app.confdir] 30 | else: 31 | parts = [""] # Config root folder 32 | parts.extend(self.pathname.split(os.path.sep)) 33 | return "/".join(parts) 34 | 35 | 36 | def PythonPythonMapper_pathname(self: PythonPythonMapper): 37 | try: 38 | slug = self.summary 39 | except AttributeError: 40 | return os.path.join(*self.name.split(".")) 41 | slug = unidecode.unidecode(slug) 42 | slug = slug.lower() 43 | slug = re.sub(r"[^\w\./]+", "-", slug).strip("-") 44 | slug_split = slug.split("/") 45 | if slug == "" or len(slug_split) == 1 or self.type == "package": 46 | return os.path.join("api", *self.name.split(".")) 47 | return os.path.join(*slug_split) 48 | 49 | 50 | def PythonSphinxMapper_output_rst( 51 | self: PythonSphinxMapper, root, source_suffix 52 | ): 53 | for _, obj in sphinx.util.status_iterator( 54 | self.objects.items(), 55 | bold("[AutoAPI] ") + "Rendering Data... ", 56 | length=len(self.objects), 57 | verbosity="INFO", 58 | stringify_func=(lambda x: x[0]), 59 | ): 60 | rst = obj.render( 61 | include_summaries=self.app.config.autoapi_include_summaries 62 | ) 63 | if not rst: 64 | continue 65 | 66 | detail_dir = obj.include_dir(root=root) 67 | ensuredir(detail_dir) 68 | path = os.path.join(detail_dir, "%s%s" % ("index", source_suffix)) 69 | open(path, "wt", encoding="utf-8").write(rst) 70 | 71 | if not hasattr(self.app, "created_api_files"): 72 | self.app.created_api_files = [] 73 | self.app.created_api_files.append(path) 74 | 75 | if not obj.pathname.startswith("api"): 76 | path_in_rst = f"/{obj.pathname.replace(os.sep, '/')}/index" 77 | index_dir = os.path.dirname(detail_dir) 78 | index_file = os.path.join(index_dir, "index" + source_suffix) 79 | if not os.path.exists(index_file): 80 | try: 81 | index_name = obj.summary.split("/")[-2] 82 | except IndexError: 83 | continue 84 | index_rst = ( 85 | f"{index_name}\n" 86 | f"{'=' * len(index_name)}\n\n" 87 | f".. toctree::\n" 88 | f" :maxdepth: 1\n" 89 | f" :sorted:\n\n" 90 | ) 91 | if index_file not in self.app.created_api_files: 92 | self.app.created_api_files.append(index_file) 93 | else: 94 | index_file_contents = open( 95 | index_file, "rt", encoding="utf-8" 96 | ).read() 97 | if path_in_rst in index_file_contents: 98 | continue 99 | index_rst = "" 100 | index_rst += f" {path_in_rst}\n" 101 | ensuredir(index_dir) 102 | open(index_file, "at+", encoding="utf-8").write(index_rst) 103 | 104 | if self.app.config.autoapi_add_toctree_entry: 105 | self._output_top_rst(root) 106 | 107 | 108 | def extension_build_finished(app, exception): 109 | if ( 110 | not app.config.autoapi_keep_files 111 | and app.config.autoapi_generate_api_docs 112 | ): 113 | if app.verbosity > 1: 114 | LOGGER.info( 115 | bold("[AutoAPI] ") + darkgreen("Cleaning generated .rst files") 116 | ) 117 | to_remove = getattr(app, "created_api_files", []) 118 | for file in to_remove: 119 | os.remove(file) 120 | directory = os.path.dirname(file) 121 | while True: 122 | try: 123 | if len(os.listdir(directory)) > 0: 124 | break 125 | os.rmdir(directory) 126 | directory = os.path.dirname(directory) 127 | except PermissionError: 128 | break 129 | 130 | 131 | def setup(app): 132 | PythonPythonMapper.pathname = property(PythonPythonMapper_pathname) 133 | PythonPythonMapper.include_dir = PythonPythonMapper_include_dir 134 | PythonSphinxMapper.output_rst = PythonSphinxMapper_output_rst 135 | autoapi.extension.build_finished = extension_build_finished 136 | autoapi_setup(app) 137 | -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/file_chooser_icon.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from kivy.lang import Builder 4 | from kivy.metrics import dp 5 | from kivy.properties import ( 6 | NumericProperty, 7 | StringProperty, 8 | ListProperty, 9 | ObjectProperty, 10 | ) 11 | from kivy.uix.filechooser import FileChooserController 12 | from kivy.utils import QueryDict 13 | 14 | Builder.load_string( 15 | """ 16 | [FileThumbEntry@BoxLayout+MDTooltip]: 17 | orientation: "vertical" 18 | spacing: "4dp" 19 | image: image 20 | locked: False 21 | path: ctx.path 22 | selected: self.path in ctx.controller().selection 23 | size_hint: None, None 24 | on_touch_down: 25 | root.entry_released_allow = True 26 | if self.collide_point(*args[1].pos): \ 27 | ctx.controller().manager.tap_on_file_dir(args, "FileChooserIcon") 28 | on_touch_up: 29 | if not ctx.controller().manager.context_menu_open: self.collide_point(*args[1].pos) \ 30 | and ctx.controller().entry_released(self, args[1]) and root.entry_released_allow 31 | size: ctx.controller().thumbsize + dp(52), ctx.controller().thumbsize + dp(52) 32 | tooltip_display_delay: 1.5 33 | on_enter: 34 | self.tooltip_text = ctx.name if ctx.controller().manager.config.getint("General", "tooltip") \ 35 | and not ctx.controller().manager.settings_panel_open and not ctx.controller().manager.dialog_plugin_open \ 36 | and not ctx.controller().manager.dialog_files_search_results_open else "" 37 | 38 | canvas: 39 | Color: 40 | rgba: 1, 1, 1, 1 if self.selected else 0 41 | BorderImage: 42 | border: 8, 8, 8, 8 43 | pos: root.pos 44 | size: root.size 45 | source: "atlas://data/images/defaulttheme/filechooser_selected" 46 | 47 | MDIconButton: 48 | id: image 49 | user_font_size: 50 | sp(int(ctx.controller().thumbsize)) \ 51 | if self.icon == "folder" else sp(int(ctx.controller().thumbsize / 2)) 52 | theme_text_color: "Custom" 53 | pos_hint: {"center_x": .5} 54 | md_bg_color_disabled: 0, 0, 0, 0 55 | text_color: 56 | app.theme_cls.primary_color if self.icon == "folder" \ 57 | else app.theme_cls.disabled_hint_text_color 58 | disabled: True if self.icon != "folder" else False 59 | 60 | MDLabel: 61 | text: ctx.name 62 | size_hint: None, None 63 | -text_size: (ctx.controller().thumbsize, self.height) 64 | halign: "center" 65 | shorten: True 66 | size: ctx.controller().thumbsize, "16dp" 67 | pos_hint: {"center_x": .5} 68 | color: ctx.controller().text_color 69 | font_style: "Caption" 70 | 71 | MDLabel: 72 | text: ctx.controller()._gen_label(ctx) 73 | font_style: "Caption" 74 | color: .8, .8, .8, 1 75 | size_hint: None, None 76 | -text_size: None, None 77 | size: ctx.controller().thumbsize, "16sp" 78 | pos_hint: {"center_x": .5} 79 | halign: "center" 80 | color: ctx.controller().text_color 81 | 82 | Widget: 83 | 84 | 85 | : 86 | on_entry_added: stack.add_widget(args[1]) 87 | on_entries_cleared: stack.clear_widgets() 88 | _scrollview: scrollview 89 | 90 | ScrollView: 91 | id: scrollview 92 | do_scroll_x: False 93 | 94 | Scatter: 95 | do_rotation: False 96 | do_scale: False 97 | do_translation: False 98 | size_hint_y: None 99 | height: stack.height 100 | 101 | StackLayout: 102 | id: stack 103 | width: scrollview.width 104 | size_hint_y: None 105 | height: self.minimum_height 106 | spacing: "10dp" 107 | padding: "10dp" 108 | """ 109 | ) 110 | 111 | 112 | class CustomFileChooserIcon(FileChooserController): 113 | _ENTRY_TEMPLATE = "FileThumbEntry" 114 | 115 | thumbsize = NumericProperty(dp(72)) 116 | """ 117 | The size of the thumbnails. 118 | 119 | :attr:`thumbsize` is an :class:`~kivy.properties.NumericProperty` 120 | and defaults to `dp(48)`. 121 | """ 122 | 123 | icon_folder = StringProperty() 124 | """ 125 | Path to icon folder. 126 | 127 | :attr:`icon_folder` is an :class:`~kivy.properties.StringProperty` 128 | and defaults to `''`. 129 | """ 130 | 131 | text_color = ListProperty() 132 | """ 133 | Label color for file and directory names. 134 | 135 | :attr:`text_color` is an :class:`~kivy.properties.ListProperty` 136 | and defaults to `[]`. 137 | """ 138 | 139 | get_icon_file = ObjectProperty() 140 | """ 141 | Method that returns the icon path for the file. 142 | 143 | :attr:`get_icon_file` is an :class:`~kivy.properties.ObjectProperty` 144 | and defaults to `None`. 145 | """ 146 | 147 | manager = ObjectProperty() 148 | """ 149 | ``MDDesktopFileManager`` object. 150 | 151 | :attr:`manager` is an :class:`~kivy.properties.ObjectProperty` 152 | and defaults to `None`. 153 | """ 154 | 155 | def __init__(self, **kwargs): 156 | super().__init__(**kwargs) 157 | self.entry_released_allow = False 158 | 159 | def entry_released(self, entry, touch): 160 | """ 161 | This method must be called by the template when an entry 162 | is touched by the user. 163 | """ 164 | 165 | # FIXME: For some reason, this method is called twice. So I had to 166 | # redefine it and include the ``entry_released_allow`` variable to 167 | # control the number of calls. 168 | if self.entry_released_allow: 169 | self.entry_released_allow = False 170 | if "button" in touch.profile and touch.button in ( 171 | "scrollup", 172 | "scrolldown", 173 | "scrollleft", 174 | "scrollright", 175 | ): 176 | return False 177 | if not self.multiselect: 178 | if self.file_system.is_dir(entry.path) and not self.dirselect: 179 | self.open_entry(entry) 180 | elif touch.is_double_tap: 181 | if self.dirselect and self.file_system.is_dir(entry.path): 182 | return 183 | else: 184 | self.dispatch("on_submit", self.selection, touch) 185 | 186 | def _create_entry_widget(self, ctx): 187 | widget = super()._create_entry_widget(ctx) 188 | kctx = QueryDict(ctx) 189 | if os.path.isdir(kctx["path"]): 190 | widget.image.icon = self.icon_folder 191 | else: 192 | if self.get_icon_file: 193 | widget.image.icon = self.get_icon_file(kctx["path"]) 194 | 195 | return widget 196 | 197 | def _gen_label(self, ctx): 198 | return ctx.get_nice_size() 199 | -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/filemanager.kv: -------------------------------------------------------------------------------- 1 | #:import os os 2 | #:import Path pathlib.Path 3 | #:import images_path kivymd.images_path 4 | #:import Clock kivy.clock.Clock 5 | #:import Window kivy.core.window.Window 6 | 7 | 8 | 9 | 10 | IconLeftWidget: 11 | icon: root.icon 12 | 13 | 14 | 15 | size_hint: .8, None 16 | height: text_field.height 17 | pos_hint: {"center_y": 0.5} 18 | 19 | MDTextFieldRect: 20 | id: text_field 21 | font_size: "13sp" 22 | size_hint_y: None 23 | multiline: False 24 | hint_text: "Search by name" 25 | height: "24dp" 26 | cursor_color: app.theme_cls.primary_color 27 | background_normal: root.background_normal 28 | background_active: root.background_normal 29 | padding: 6, (self.height / 2) - (self.line_height / 2), lbl_icon_right.size[0], 6 30 | on_text_validate: root.on_enter(self, self.text) 31 | 32 | MDIconButton: 33 | id: lbl_icon_right 34 | icon: "cog" 35 | ripple_scale: .5 36 | pos_hint: {"center_y": .5} 37 | user_font_size: "18sp" 38 | pos: text_field.width - self.width + dp(8), 0 39 | on_release: root.context_menu_search_field.open() 40 | 41 | 42 | 43 | size_hint: None, .7 44 | width: Window.width * 70 / 100 45 | 46 | MDBoxLayout: 47 | orientation: "vertical" 48 | 49 | MDBoxLayout: 50 | adaptive_height: True 51 | padding: "12sp" 52 | 53 | MDIconButton 54 | icon: "magnify" 55 | disabled: True 56 | 57 | MDLabel: 58 | text: "Found in:" 59 | bold: True 60 | 61 | MDSeparator: 62 | 63 | RecycleView: 64 | id: rv 65 | key_viewclass: "viewclass" 66 | key_size: "height" 67 | 68 | RecycleBoxLayout: 69 | padding: "10dp" 70 | default_size: None, dp(48) 71 | default_size_hint: 1, None 72 | size_hint_y: None 73 | height: self.minimum_height 74 | orientation: "vertical" 75 | 76 | 77 | 78 | size_hint: None, None 79 | height: container.height 80 | width: Window.width * 70 / 100 81 | auto_dismiss: False 82 | 83 | MDBoxLayout: 84 | id: container 85 | orientation: "vertical" 86 | padding: "12dp" 87 | spacing: "12dp" 88 | adaptive_height: True 89 | 90 | MDBoxLayout: 91 | padding: "12dp" 92 | spacing: "12dp" 93 | adaptive_height: True 94 | 95 | MDSpinner: 96 | size_hint: None, None 97 | size: "48dp", "48dp" 98 | 99 | MDBoxLayout: 100 | orientation: "vertical" 101 | spacing: "24dp" 102 | 103 | MDLabel: 104 | id: lbl_dir 105 | shorten: True 106 | 107 | MDLabel: 108 | id: lbl_file 109 | shorten: True 110 | 111 | MDBoxLayout: 112 | adaptive_height: True 113 | 114 | MDBoxLayout: 115 | adaptive_size: True 116 | spacing: "12sp" 117 | 118 | MDCheckbox: 119 | id: check_background 120 | size_hint: None, None 121 | size: "48dp", "48dp" 122 | on_active: 123 | if self.active: Clock.schedule_once(lambda x: root.dismiss(), .5); \ 124 | root.manager.show_taskbar() 125 | 126 | 127 | MDLabel: 128 | text: "In background" 129 | size_hint: None, None 130 | -text_size: None, None 131 | size: self.texture_size 132 | pos_hint: {"center_y": .5} 133 | 134 | Widget: 135 | 136 | MDRaisedButton: 137 | text: "CANCEL" 138 | on_release: 139 | root.dismiss() 140 | root.manager.instance_search_field.canceled_search = True 141 | 142 | 143 | 144 | manager: None 145 | on_release: 146 | app.theme_cls.primary_palette = root.text 147 | if root.manager: root.manager.config.set("General", "palette", root.text); \ 148 | root.manager.config.write() 149 | height: "36dp" 150 | font_style: "Caption" 151 | _txt_bot_pad: "10dp" 152 | 153 | FileManagerSettingsLeftWidgetItem: 154 | size_hint: None, None 155 | size: "24dp", "24dp" 156 | pos_hint: {"center_y": .5} 157 | 158 | canvas.before: 159 | Color: 160 | rgba: root.color 161 | Ellipse: 162 | pos: self.pos 163 | size: self.size 164 | 165 | 166 | 167 | size_hint_y: None 168 | height: "180dp" 169 | 170 | RecycleView: 171 | id: rv 172 | key_viewclass: 'viewclass' 173 | key_size: 'height' 174 | 175 | RecycleBoxLayout: 176 | default_size: None, dp(36) 177 | default_size_hint: 1, None 178 | size_hint_y: None 179 | height: self.minimum_height 180 | orientation: 'vertical' 181 | 182 | 183 | 184 | orientation: "vertical" 185 | adaptive_height: True 186 | padding: "12dp" 187 | spacing: "8dp" 188 | manager: None 189 | 190 | MDBoxLayout: 191 | spacing: "4dp" 192 | size_hint_y: None 193 | height: "36dp" 194 | 195 | MDCheckbox: 196 | id: tooltip_check 197 | size_hint: None, None 198 | size: "36dp", "36dp" 199 | on_active: 200 | if root.manager: root.manager.config.set("General", "tooltip", int(self.active)); \ 201 | root.manager.config.write() 202 | 203 | MDLabel: 204 | text: "Use tooltip for filenames" 205 | size_hint_y: None 206 | height: self.texture_size[1] 207 | pos_hint: {"center_y": .5} 208 | font_style: "Caption" 209 | shorten: True 210 | 211 | MDSeparator: 212 | 213 | MDBoxLayout: 214 | spacing: "4dp" 215 | size_hint_y: None 216 | height: "36dp" 217 | 218 | MDCheckbox: 219 | id: memorize_check 220 | size_hint: None, None 221 | size: "36dp", "36dp" 222 | on_active: 223 | if root.manager: root.manager.config.set("General", "memorize_palette", int(self.active)); \ 224 | root.manager.config.write() 225 | 226 | MDLabel: 227 | text: "Memorize palette" 228 | size_hint_y: None 229 | height: self.texture_size[1] 230 | pos_hint: {"center_y": .5} 231 | font_style: "Caption" 232 | shorten: True 233 | 234 | MDSeparator: 235 | 236 | MDBoxLayout: 237 | adaptive_height: True 238 | padding: "12dp", 0, 0, 0 239 | spacing: "24dp" 240 | 241 | MDSwitch: 242 | id: theme_switch 243 | on_active: 244 | app.theme_cls.theme_style = "Dark" if self.active else "Light" 245 | root.manager.config.set("General", "theme", app.theme_cls.theme_style) 246 | root.manager.config.write() 247 | 248 | MDLabel: 249 | text: "Theme {}".format("Dark" if app.theme_cls.theme_style == "Dark" else "Light") 250 | size_hint_y: None 251 | height: self.texture_size[1] 252 | pos_hint: {"center_y": .5} 253 | font_style: "Caption" 254 | shorten: True 255 | 256 | MDSeparator: 257 | 258 | 259 | 260 | 261 | MDRelativeLayout: 262 | 263 | MDRelativeLayout: 264 | canvas.after: 265 | Color: 266 | rgba: root._overlay_color 267 | Rectangle: 268 | pos: self.pos 269 | size: self.size 270 | 271 | MDBoxLayout: 272 | orientation: "vertical" 273 | padding: "5dp" 274 | md_bg_color: root.theme_cls.bg_normal if not root.bg_color else root.bg_color 275 | 276 | canvas: 277 | Rectangle: 278 | pos: self.pos 279 | size: self.size 280 | source: 281 | os.path.join(images_path, "transparent.png") \ 282 | if not root.bg_texture else root.bg_texture 283 | 284 | MDBoxLayout: 285 | id: header_box_menu 286 | adaptive_height: True 287 | spacing: "4dp" 288 | md_bg_color: 289 | (0, 0, 0, 0) if root.bg_texture \ 290 | else (root.theme_cls.bg_dark if not root.bg_color else root.bg_color) 291 | 292 | MDBoxLayout: 293 | adaptive_height: True 294 | spacing: "10dp" 295 | 296 | MDTextField: 297 | id: field_current_path 298 | font_size: "13sp" 299 | text: root.path 300 | selection_color: root.theme_cls.primary_color[:-1] + [.3] 301 | 302 | MDTabs: 303 | id: tabs 304 | tab_bar_height: "24dp" 305 | background_color: header_box_menu.md_bg_color 306 | color_indicator: app.theme_cls.primary_color 307 | text_color_normal: app.theme_cls.text_color 308 | text_color_active: app.theme_cls.primary_color 309 | elevation: 0 310 | tab_indicator_height: 2 311 | on_ref_press: root.remove_tab(*args) 312 | on_tab_switch: root._on_tab_switch(*args) 313 | 314 | MDBoxLayout: 315 | id: taskbar 316 | size_hint_y: None 317 | height: 0 318 | padding: 0, "2dp", 0, "2dp" 319 | 320 | MDBoxLayout: 321 | md_bg_color: app.theme_cls.bg_light 322 | padding: "12dp", 0, "12dp", 0 323 | spacing: "12dp" 324 | 325 | MDSpinner: 326 | id: task_spinner 327 | size_hint: None, None 328 | size: "10dp", "10dp" 329 | pos_hint: {"center_y": .5} 330 | active: False 331 | opacity: 0 332 | 333 | MDLabel: 334 | id: lbl_task 335 | font_style: "Caption" 336 | shorten: True 337 | markup: True 338 | opacity: 0 339 | 340 | MDIconButton: 341 | id: button_expand 342 | icon: "arrow-up-bold-box-outline" 343 | user_font_size: "1sp" 344 | pos_hint: {"center_y": .5} 345 | ripple_scale: .4 346 | on_release: root.hide_taskbar() 347 | 348 | MDCard: 349 | id: settings_container 350 | size_hint_y: None 351 | size_hint_x: .75 352 | pos_hint: {"center_x": .5} 353 | height: settings.height 354 | md_bg_color: root.theme_cls.bg_darkest 355 | settings_container_y: 0 356 | y: root.height - self.settings_container_y 357 | elevation: 12 358 | radius: [0, 0, 8, 8, ] 359 | 360 | FileManagerSettings: 361 | id: settings 362 | manager: root 363 | 364 | 365 | 366 | padding: 0, "2dp", 0, 0 367 | 368 | Splitter: 369 | direction: "right-left" 370 | sizable_from: "right" 371 | rescale_with_parent: False 372 | size_hint_x: .4 373 | strip_size: "5dp" 374 | 375 | MDBoxLayout: 376 | md_bg_color: app.theme_cls.bg_dark 377 | 378 | # Directory tree on the left. 379 | FileChooserListView: 380 | id: file_chooser_list 381 | path: str(Path.home()) 382 | filters: [root.manager.is_dir] 383 | callback: root.manager.set_path 384 | manager: root.manager 385 | 386 | canvas.before: 387 | Color: 388 | rgba: 389 | root.manager.theme_cls.bg_dark \ 390 | if not root.manager.bg_color \ 391 | else root.manager.bg_color 392 | Rectangle: 393 | pos: self.pos 394 | size: self.size 395 | 396 | CustomFileChooserIcon: 397 | id: file_chooser_icon 398 | icon_folder: 399 | os.path.join(root.manager.path_to_skin, "folder.png") \ 400 | if root.manager.path_to_skin else "folder" 401 | get_icon_file: root.manager.get_icon_file 402 | text_color: app.theme_cls.text_color 403 | path: root.manager.path if not root.path else root.path 404 | manager: root.manager 405 | 406 | 407 | 408 | text: root.text 409 | font_style: "Caption" 410 | _txt_left_pad: "16dp" 411 | _txt_bot_pad: "10dp" 412 | 413 | IconRightWidget: 414 | icon: root.icon 415 | user_font_size: "20sp" 416 | disabled: True 417 | -------------------------------------------------------------------------------- /kivymd_extensions/filemanager/filemanager.py: -------------------------------------------------------------------------------- 1 | """ 2 | Components/File Manager 3 | ======================= 4 | 5 | File manager for the desktop. 6 | 7 | .. image:: https://github.com/kivymd/storage/raw/main/filemanager/images/preview.png 8 | :align: center 9 | 10 | .. warning:: 11 | 12 | We have not tested the file manager for Windows OS. Therefore, we do not 13 | guarantee the absence of bugs in this OS. Contact `technical support `_ if you 14 | have any problems using the file manager. 15 | 16 | Usage 17 | ----- 18 | 19 | .. code-block:: python 20 | 21 | from kivymd.app import MDApp 22 | 23 | from kivymd_extensions.filemanager import FileManager 24 | 25 | 26 | class Example(MDApp): 27 | def on_start(self): 28 | FileManager().open() 29 | 30 | 31 | Example().run() 32 | 33 | Customization 34 | ============= 35 | 36 | If you want to use custom icons in the file manager, you need to specify the path to the icon theme: 37 | 38 | .. code-block:: python 39 | 40 | FileManager(path_to_skin="path/to/images").open() 41 | 42 | .. image:: https://github.com/kivymd/storage/raw/main/filemanager/images/customization.png 43 | :align: center 44 | 45 | The resource directory structure for the file manager theme looks like this: 46 | 47 | .. image:: https://github.com/kivymd/storage/raw/main/filemanager/images/skin-structure.png 48 | :align: center 49 | 50 | The ``files`` directory contains images whose names correspond to the file types: 51 | 52 | .. image:: https://github.com/kivymd/storage/raw/main/filemanager/images/skin-structure-files.png 53 | :align: center 54 | 55 | Color 56 | ----- 57 | 58 | .. code-block:: python 59 | 60 | FileManager(bg_color=(1, 0, 0, 0.2)).open() 61 | 62 | .. image:: https://github.com/kivymd/storage/raw/main/filemanager/images/bg-color.png 63 | :align: center 64 | 65 | Texture 66 | ------- 67 | 68 | .. code-block:: python 69 | 70 | FileManager(bg_texture="path/to/texture.png").open() 71 | 72 | .. image:: https://github.com/kivymd/storage/raw/main/filemanager/images/bg-texture.png 73 | :align: center 74 | 75 | .. warning:: 76 | 77 | If you are using ``bg_texture`` parameter then ``bg_color`` parameter will be ignored. 78 | 79 | Events 80 | ====== 81 | 82 | `on_tab_switch` 83 | Called when switching tabs. 84 | `on_tap_file` 85 | Called when the file is clicked. 86 | `on_tap_dir` 87 | Called when the folder is clicked. 88 | `on_context_menu` 89 | Called at the end of any actions of the context menu, 90 | be it copying, archiving files and other actions. 91 | 92 | .. code-block:: 93 | 94 | from kivymd.app import MDApp 95 | 96 | from kivymd_extensions.filemanager import FileManager 97 | 98 | 99 | class Example(MDApp): 100 | def on_context_menu(self, instance_file_manager, name_context_plugin): 101 | print("Event 'on_context_menu'", instance_file_manager, name_context_plugin) 102 | 103 | def on_tap_file(self, instance_file_manager, path): 104 | print("Event 'on_tap_file'", instance_file_manager, path) 105 | 106 | def on_tap_dir(self, instance_file_manager, path): 107 | print("Event 'on_tap_dir'", instance_file_manager, path) 108 | 109 | def on_tab_switch( 110 | self, 111 | instance_file_manager, 112 | instance_tabs, 113 | instance_tab, 114 | instance_tab_label, 115 | tab_text, 116 | ): 117 | print( 118 | "Event 'on_tab_switch'", 119 | instance_file_manager, 120 | instance_tabs, 121 | instance_tab, 122 | instance_tab_label, 123 | tab_text, 124 | ) 125 | 126 | def build(self): 127 | self.theme_cls.primary_palette = "Red" 128 | 129 | def on_start(self): 130 | manager = FileManager() 131 | manager.bind( 132 | on_tap_file=self.on_tap_file, 133 | on_tap_dir=self.on_tap_dir, 134 | on_tab_switch=self.on_tab_switch, 135 | on_context_menu=self.on_context_menu, 136 | ) 137 | manager.open() 138 | 139 | 140 | Example().run() 141 | """ 142 | 143 | import re 144 | import ast 145 | import importlib 146 | import os 147 | import threading 148 | 149 | from kivy.factory import Factory 150 | from kivy.animation import Animation 151 | from kivy.clock import Clock 152 | from kivy.core.window import Window 153 | from kivy.lang import Builder 154 | from kivy.metrics import dp, sp 155 | from kivy.properties import ( 156 | StringProperty, 157 | ObjectProperty, 158 | BooleanProperty, 159 | ListProperty, 160 | OptionProperty, 161 | DictProperty, 162 | ) 163 | from kivy.uix.boxlayout import BoxLayout 164 | from kivy.config import Config, ConfigParser 165 | from kivy.uix.widget import Widget 166 | from kivy.utils import get_color_from_hex, get_hex_from_color 167 | 168 | Config.set("input", "mouse", "mouse,disable_multitouch") 169 | 170 | from kivymd.uix.menu import MDDropdownMenu 171 | from kivymd.uix.tab import MDTabsBase 172 | from kivymd.uix.button import MDIconButton, MDFlatButton 173 | from kivymd.uix.list import ( 174 | OneLineAvatarIconListItem, 175 | OneLineListItem, 176 | OneLineAvatarListItem, 177 | ILeftBody, 178 | ) 179 | from kivymd.uix.behaviors import HoverBehavior 180 | from kivymd.theming import ThemableBehavior 181 | from kivymd.uix.dialog import BaseDialog 182 | from kivymd.font_definitions import fonts 183 | from kivymd.icon_definitions import md_icons 184 | from kivymd.uix.boxlayout import MDBoxLayout 185 | from kivymd.uix.card import MDSeparator 186 | from kivymd.color_definitions import palette 187 | from kivymd.color_definitions import colors 188 | from kivymd.uix.expansionpanel import MDExpansionPanel 189 | from kivymd.uix.expansionpanel import MDExpansionPanelOneLine 190 | from kivymd.uix.relativelayout import MDRelativeLayout 191 | 192 | from kivymd_extensions.filemanager.libs.plugins import PluginBaseDialog 193 | 194 | with open( 195 | os.path.join(os.path.dirname(__file__), "file_chooser_list.kv"), 196 | encoding="utf-8", 197 | ) as kv: 198 | Builder.load_string(kv.read()) 199 | with open( 200 | os.path.join(os.path.dirname(__file__), "custom_splitter.kv"), 201 | encoding="utf-8", 202 | ) as kv: 203 | Builder.load_string(kv.read()) 204 | with open( 205 | os.path.join(os.path.dirname(__file__), "filemanager.kv"), 206 | encoding="utf-8", 207 | ) as kv: 208 | Builder.load_string(kv.read()) 209 | 210 | 211 | class FileManagerItem(OneLineAvatarIconListItem): 212 | icon = StringProperty() 213 | 214 | 215 | class FileManagerTab(BoxLayout, MDTabsBase): 216 | """Class implementing content for a tab.""" 217 | 218 | manager = ObjectProperty() 219 | """ 220 | :class:`FileManager` object. 221 | """ 222 | 223 | path = StringProperty() 224 | """ 225 | Path to root directory for instance :attr:`manager`. 226 | 227 | :attr:`path` is an :class:`~kivy.properties.StringProperty` 228 | and defaults to `''`. 229 | """ 230 | 231 | 232 | class FileManagerTextFieldSearch(ThemableBehavior, MDRelativeLayout): 233 | """The class implements a text field for searching files. 234 | 235 | See rule ``FileManagerTextFieldSearch`` 236 | in ``kivymd_extensions/filemanager/filemanager.kv file``. 237 | """ 238 | 239 | hint_text = StringProperty() 240 | """ 241 | See :attr:`~kivy.uix.textinput.TextInput.hint_text 242 | """ 243 | 244 | background_normal = StringProperty() 245 | """ 246 | See :attr:`~kivy.uix.textinput.TextInput.background_normal 247 | """ 248 | 249 | background_active = StringProperty() 250 | """ 251 | See :attr:`~kivy.uix.textinput.TextInput.background_active 252 | """ 253 | 254 | type = OptionProperty("name", options=["name", "ext"]) 255 | """ 256 | Search files by name or extension. Available options are `'name'`, `'ext'`. 257 | 258 | :attr:`icon` is an :class:`~kivy.properties.OptionProperty` 259 | and defaults to `'name'`. 260 | """ 261 | 262 | manager = ObjectProperty() 263 | """ 264 | See :class:`FileManager` object. 265 | """ 266 | 267 | def __init__(self, **kw): 268 | super().__init__(**kw) 269 | # Signal to interrupt the search process 270 | self.canceled_search = False 271 | # 272 | self.text_field_search_dialog = None 273 | # 274 | self.context_menu_search_field = None 275 | # Whether to search the entire disk or the current directory. 276 | self.search_all_disk = False 277 | self.end_search = False 278 | Clock.schedule_once(self.create_menu) 279 | 280 | def on_enter(self, instance, value): 281 | """Called when the user hits 'Enter' in text field.""" 282 | 283 | def wait_result(interval): 284 | if self.end_search: 285 | Clock.unschedule(wait_result) 286 | if data_results: 287 | FileManagerFilesSearchResultsDialog( 288 | data_results=data_results, manager=self.manager 289 | ).open() 290 | self.manager.dialog_files_search_results_open = True 291 | 292 | def start_search(interval): 293 | threading.Thread( 294 | target=get_matching_files, 295 | args=( 296 | "/" if self.search_all_disk else self.manager.path, 297 | value, 298 | ), 299 | ).start() 300 | 301 | def get_matching_files(path, name_file): 302 | for d, dirs, files in os.walk(path): 303 | if self.canceled_search: 304 | break 305 | self.text_field_search_dialog.ids.lbl_dir.text = d 306 | for f in files: 307 | self.text_field_search_dialog.ids.lbl_file.text = f 308 | self.manager.ids.lbl_task.text = ( 309 | f"Search in [color=" 310 | f"{get_hex_from_color(self.theme_cls.primary_color)}]" 311 | f"{os.path.dirname(d)}:[/color] {f}" 312 | ) 313 | if self.ids.text_field.hint_text == "Search by name": 314 | if name_file in f: 315 | data_results[f] = os.path.join(d, f) 316 | elif self.ids.text_field.hint_text == "Search by extension": 317 | if f.endswith(name_file): 318 | data_results[f] = os.path.join(d, f) 319 | if self.canceled_search: 320 | self.canceled_search = False 321 | self.end_search = True 322 | self.text_field_search_dialog.dismiss() 323 | 324 | self.text_field_search_dialog = FileManagerTextFieldSearchDialog( 325 | manager=self.manager 326 | ) 327 | self.text_field_search_dialog.open() 328 | self.end_search = False 329 | data_results = {} 330 | Clock.schedule_once(start_search, 1) 331 | Clock.schedule_interval(wait_result, 0) 332 | 333 | def create_menu(self, interval): 334 | menu = [] 335 | for text in ( 336 | "Search by extension", 337 | "Search by name", 338 | "All over the disk", 339 | ): 340 | menu.append( 341 | { 342 | "text": f"[size=14]{text}[/size]", 343 | "viewclass": "OneLineListItem", 344 | "height": dp(36), 345 | "top_pad": dp(4), 346 | "bot_pad": dp(10), 347 | "divider": None, 348 | "on_release": lambda x=f"[size=14]{text}[/size]": self.set_type_search( 349 | x 350 | ), 351 | } 352 | ) 353 | self.context_menu_search_field = MDDropdownMenu( 354 | caller=self.ids.lbl_icon_right, 355 | items=menu, 356 | width_mult=4, 357 | background_color=self.theme_cls.bg_dark, 358 | max_height=dp(240), 359 | ) 360 | 361 | def set_type_search(self, text_item): 362 | self.context_menu_search_field.dismiss() 363 | self.search_all_disk = False 364 | item_text = re.sub("\[size\s*([^\]]+)\]", "", text_item) 365 | item_text = re.sub("\[/size\s*\]", "", item_text) 366 | if item_text == "Search by extension": 367 | self.type = "ext" 368 | elif item_text == "Search by name": 369 | self.type = "name" 370 | elif item_text == "All over the disk": 371 | self.search_all_disk = True 372 | self.ids.text_field.hint_text += f" {item_text.lower()}" 373 | return 374 | self.ids.text_field.hint_text = item_text 375 | 376 | 377 | class FileManagerFilesSearchResultsDialog(PluginBaseDialog): 378 | """ 379 | The class implements displaying a list with the results of file search. 380 | """ 381 | 382 | data_results = DictProperty() 383 | 384 | manager = ObjectProperty() 385 | """ 386 | See :class:`FileManager` object. 387 | """ 388 | 389 | def on_open(self): 390 | self.ids.rv.data = [] 391 | for name_file in self.data_results.keys(): 392 | text = ( 393 | f"[color={get_hex_from_color(self.theme_cls.primary_color)}]" 394 | f"{name_file}[/color] {self.data_results[name_file]}" 395 | ) 396 | self.ids.rv.data.append( 397 | { 398 | "viewclass": "OneLineListItem", 399 | "text": text, 400 | "on_release": lambda x=self.data_results[ 401 | name_file 402 | ]: self.go_to_directory_found_file(x), 403 | } 404 | ) 405 | 406 | def on_dismiss(self): 407 | self.manager.dialog_files_search_results_open = False 408 | 409 | def go_to_directory_found_file(self, path_to_found_file): 410 | self.manager.add_tab(os.path.dirname(path_to_found_file)) 411 | 412 | 413 | class FileManagerSettingsLeftWidgetItem(ILeftBody, Widget): 414 | pass 415 | 416 | 417 | class FileManagerSettingsColorItem(OneLineAvatarListItem): 418 | color = ListProperty() 419 | 420 | 421 | class FileManagerSettings(MDBoxLayout): 422 | pass 423 | 424 | 425 | class FileManagerTextFieldSearchDialog(PluginBaseDialog): 426 | manager = ObjectProperty() 427 | 428 | 429 | class ContextMenuBehavior(ThemableBehavior, HoverBehavior): 430 | def on_enter(self): 431 | self.bg_color = ( 432 | self.theme_cls.bg_light 433 | if self.theme_cls.theme_style == "Dark" 434 | else self.theme_cls.bg_darkest 435 | ) 436 | 437 | def on_leave(self): 438 | self.bg_color = self.theme_cls.bg_normal 439 | 440 | 441 | class ContextMenuItemMore(OneLineAvatarIconListItem, ContextMenuBehavior): 442 | """Context menu item.""" 443 | 444 | icon = StringProperty() 445 | """ 446 | Icon of item. 447 | 448 | :attr:`icon` is an :class:`~kivy.properties.StringProperty` 449 | and defaults to `''`. 450 | """ 451 | 452 | def __init__(self, **kwargs): 453 | super().__init__(**kwargs) 454 | self.ids._right_container.size = (dp(24), dp(48)) 455 | 456 | 457 | class ContextMenuItem(OneLineListItem, ContextMenuBehavior): 458 | """Context menu item.""" 459 | 460 | icon = StringProperty() 461 | """ 462 | Icon of item. 463 | 464 | :attr:`icon` is an :class:`~kivy.properties.StringProperty` 465 | and defaults to `''`. 466 | """ 467 | 468 | def __init__(self, **kwargs): 469 | super().__init__(**kwargs) 470 | self._txt_left_pad = dp(16) 471 | self._txt_bot_pad = dp(10) 472 | 473 | 474 | class FileManager(BaseDialog): 475 | """ 476 | :Events: 477 | `on_tab_switch` 478 | Called when switching tabs. 479 | `on_tap_file` 480 | Called when the file is clicked. 481 | `on_tap_dir` 482 | Called when the folder is clicked. 483 | `on_context_menu` 484 | Called at the end of any actions of the context menu, 485 | be it copying, archiving files and other actions. 486 | `on_open_plugin_dialog` 487 | Description. 488 | `on_dismiss_plugin_dialog` 489 | Description. 490 | """ 491 | 492 | with open( 493 | os.path.join( 494 | os.path.dirname(__file__), "data", "context-menu-items.json" 495 | ), 496 | encoding="utf-8", 497 | ) as data: 498 | menu_right_click_items = ast.literal_eval(data.read()) 499 | 500 | path = StringProperty(os.getcwd()) 501 | """ 502 | The path to the directory in which the file manager will open by 503 | default. 504 | 505 | :attr:`path` is an :class:`~kivy.properties.StringProperty` 506 | and defaults to ``os.getcwd()``. 507 | """ 508 | 509 | context_menu_open = BooleanProperty(False) 510 | """ 511 | Open or close context menu. 512 | 513 | :attr:`context_menu_open` is an :class:`~kivy.properties.BooleanProperty` 514 | and defaults to `False`. 515 | """ 516 | 517 | path_to_skin = StringProperty() 518 | """ 519 | Path to directory with custom images. 520 | 521 | :attr:`path_to_skin` is an :class:`~kivy.properties.StringProperty` 522 | and defaults to `''`. 523 | """ 524 | 525 | bg_color = ListProperty() 526 | """ 527 | Background color of file manager in the format (r, g, b, a). 528 | 529 | :attr:`bg_color` is a :class:`~kivy.properties.ListProperty` 530 | and defaults to `[]`. 531 | """ 532 | 533 | bg_texture = StringProperty() 534 | """ 535 | Background texture of file manager. 536 | 537 | :attr:`bg_texture` is a :class:`~kivy.properties.StringProperty` and 538 | defaults to `''`. 539 | """ 540 | 541 | _overlay_color = ListProperty([0, 0, 0, 0]) 542 | 543 | auto_dismiss = False 544 | 545 | _instance_file_chooser_icon = None 546 | 547 | def __init__(self, **kwargs): 548 | super().__init__(**kwargs) 549 | self.ext_files = {} 550 | # The object of the currently open tab. 551 | self.current_open_tab_manager = None 552 | # Open or closed the settings panel. 553 | self.settings_panel_open = False 554 | # Open or close the theme selection panel in the settings panel. 555 | self.settings_theme_panel_open = False 556 | # Open or close the dialog of plugin. 557 | self.dialog_plugin_open = False 558 | # Open or close dialog with search results. 559 | self.dialog_files_search_results_open = False 560 | 561 | self.instance_search_field = None 562 | 563 | self.config = ConfigParser() 564 | self.data_dir = os.path.join(os.path.dirname(__file__), "data") 565 | self.config.read(os.path.join(self.data_dir, "settings.ini")) 566 | 567 | self.register_event_type("on_tab_switch") 568 | self.register_event_type("on_tap_file") 569 | self.register_event_type("on_tap_dir") 570 | self.register_event_type("on_context_menu") 571 | self.register_event_type("on_open_plugin_dialog") 572 | self.register_event_type("on_dismiss_plugin_dialog") 573 | 574 | self.theme_cls.bind(theme_style=self.update_background_search_field) 575 | 576 | if self.path_to_skin and os.path.exists(self.path_to_skin): 577 | path_to_directory_exts = os.path.join(self.path_to_skin, "files") 578 | for name_file in os.listdir(path_to_directory_exts): 579 | self.ext_files[name_file.split(".")[0]] = os.path.join( 580 | path_to_directory_exts, name_file 581 | ) 582 | if not self.ext_files: 583 | with open( 584 | os.path.join( 585 | os.path.dirname(__file__), "data", "default_files_type.json" 586 | ), 587 | encoding="utf-8", 588 | ) as data: 589 | self.ext_files = ast.literal_eval(data.read()) 590 | 591 | def add_color_panel(self): 592 | def set_list_colors_themes(*args): 593 | self.settings_theme_panel_open = True 594 | if not theme_panel.content.ids.rv.data: 595 | for name_theme in palette: 596 | theme_panel.content.ids.rv.data.append( 597 | { 598 | "viewclass": "FileManagerSettingsColorItem", 599 | "color": get_color_from_hex( 600 | colors[name_theme]["500"] 601 | ), 602 | "text": name_theme, 603 | "manager": self, 604 | } 605 | ) 606 | 607 | # Adds a panel. 608 | theme_panel = MDExpansionPanel( 609 | icon="palette", 610 | content=Factory.FileManagerChangeTheme(), 611 | panel_cls=MDExpansionPanelOneLine(text="Select theme"), 612 | ) 613 | theme_panel.bind( 614 | on_open=set_list_colors_themes, 615 | on_close=self._set_state_close_theme_panel, 616 | ) 617 | self.ids.settings.add_widget(theme_panel) 618 | 619 | # Adds a close button to the settings panel. 620 | box = MDBoxLayout(adaptive_height=True) 621 | box.add_widget(Widget()) 622 | box.add_widget( 623 | MDFlatButton( 624 | text="CLOSE", 625 | on_release=lambda x: self.hide_settings(theme_panel), 626 | ) 627 | ) 628 | self.ids.settings.add_widget(box) 629 | 630 | def apply_palette(self): 631 | """Applies the color theme from the settings file when opening the 632 | file manager window.""" 633 | 634 | palette = self.config.get("General", "palette") 635 | theme = self.config.get("General", "theme") 636 | memorize_palette = self.config.getint("General", "memorize_palette") 637 | 638 | if memorize_palette: 639 | self.theme_cls.primary_palette = palette 640 | self.theme_cls.theme_style = theme 641 | 642 | def apply_properties_on_show_settings(self): 643 | """Applies the settings from the "settings.ini" file to the checkboxes 644 | of items on the settings panel.""" 645 | 646 | self.ids.settings.ids.tooltip_check.active = self.config.getint( 647 | "General", "tooltip" 648 | ) 649 | self.ids.settings.ids.memorize_check.active = self.config.getint( 650 | "General", "memorize_palette" 651 | ) 652 | self.ids.settings.ids.theme_switch.active = ( 653 | 1 if self.theme_cls.theme_style == "Dark" else 0 654 | ) 655 | 656 | def show_taskbar(self): 657 | def on_complete_animation(*args): 658 | self.ids.task_spinner.active = True 659 | self.ids.lbl_task.opacity = 1 660 | 661 | Animation(height=dp(24), d=0.2).start(self.ids.taskbar) 662 | Animation(user_font_size=sp(18), d=0.2).start(self.ids.button_expand) 663 | anim = Animation(opacity=1, d=0.2) 664 | anim.bind(on_complete=on_complete_animation) 665 | anim.start(self.ids.task_spinner) 666 | 667 | def hide_taskbar(self): 668 | def on_complete_animation(*args): 669 | self.ids.task_spinner.active = False 670 | self.ids.lbl_task.opacity = 0 671 | self.instance_search_field.text_field_search_dialog.ids.check_background.active = ( 672 | False 673 | ) 674 | self.instance_search_field.text_field_search_dialog.open() 675 | 676 | Animation(height=0, d=0.2).start(self.ids.taskbar) 677 | Animation(user_font_size=sp(1), d=0.2).start(self.ids.button_expand) 678 | anim = Animation(opacity=1, d=0.2) 679 | anim.bind(on_complete=on_complete_animation) 680 | anim.start(self.ids.task_spinner) 681 | 682 | def show_settings(self, instance_button): 683 | """Opens the settings panel.""" 684 | 685 | self.apply_properties_on_show_settings() 686 | Animation( 687 | settings_container_y=self.ids.settings.height, 688 | d=0.2, 689 | ).start(self.ids.settings_container) 690 | Animation( 691 | _overlay_color=[0, 0, 0, 0.4], 692 | d=0.2, 693 | ).start(self) 694 | self.settings_panel_open = True 695 | 696 | def hide_settings(self, theme_panel): 697 | """Closes the settings panel.""" 698 | 699 | def hide_settings(interval): 700 | Animation(settings_container_y=0, d=0.2).start( 701 | self.ids.settings_container 702 | ) 703 | self._set_state_close_theme_panel() 704 | 705 | if self.settings_theme_panel_open: 706 | theme_panel.check_open_panel(theme_panel) 707 | Clock.schedule_once(hide_settings, 0.5) 708 | Animation( 709 | _overlay_color=[0, 0, 0, 0], 710 | d=0.2, 711 | ).start(self) 712 | self.settings_panel_open = False 713 | 714 | def set_path(self, path): 715 | """Sets the directory path for the `FileChooserIconLayout` class.""" 716 | 717 | self.path = path 718 | self.current_open_tab_manager.ids.file_chooser_icon.path = path 719 | tab_text = self.get_formatting_text_for_tab(os.path.split(path)[1]) 720 | self.current_open_tab_manager.text = tab_text 721 | 722 | def get_formatting_text_for_tab(self, text): 723 | icon_font = fonts[-1]["fn_regular"] 724 | icon = md_icons["close"] 725 | text = f"[size=16][font={icon_font}][ref=]{icon}[/ref][/size][/font] {text}" 726 | return text 727 | 728 | def add_tab(self, path_to_file): 729 | """ 730 | Adds a new tab in the file manager. 731 | 732 | :param path_to_file: The path to the file or folder that was right-clicked. 733 | """ 734 | 735 | tab_text = self.get_formatting_text_for_tab( 736 | os.path.split(path_to_file)[1] 737 | ) 738 | tab = FileManagerTab(manager=self, title=tab_text, path=path_to_file) 739 | self._instance_file_chooser_icon = tab.ids.file_chooser_icon 740 | self.ids.tabs.add_widget(tab) 741 | self.current_open_tab_manager = tab 742 | self.ids.tabs.switch_tab(tab_text, search_by="title") 743 | self.path = path_to_file 744 | 745 | def remove_tab( 746 | self, 747 | instance_tabs, 748 | instance_tab_label, 749 | instance_tab, 750 | instance_tab_bar, 751 | instance_carousel, 752 | ): 753 | """Removes an open tab in the file manager. 754 | 755 | :param instance_tabs: 756 | :param instance_tab_label: 757 | :param instance_tab: <__main__.Tab object> 758 | :param instance_tab_bar: 759 | :param instance_carousel: 760 | """ 761 | 762 | for instance_tab in instance_carousel.slides: 763 | if instance_tab.text == instance_tab_label.text: 764 | instance_tabs.remove_widget(instance_tab_label) 765 | break 766 | 767 | def create_header_menu(self): 768 | """Creates a menu in the file manager header.""" 769 | 770 | with open( 771 | os.path.join(os.path.dirname(__file__), "data", "header_menu.json"), 772 | encoding="utf-8", 773 | ) as data: 774 | menu_header = ast.literal_eval(data.read()) 775 | for name_icon_item in menu_header: 776 | self.ids.header_box_menu.add_widget( 777 | MDIconButton( 778 | icon=name_icon_item, 779 | user_font_size="18sp", 780 | disabled=True 781 | if name_icon_item not in ("home", "settings") 782 | else False, 783 | md_bg_color_disabled=(0, 0, 0, 0), 784 | ) 785 | ) 786 | self.ids.header_box_menu.add_widget(MDSeparator(orientation="vertical")) 787 | self.ids.header_box_menu.add_widget( 788 | MDIconButton( 789 | icon="cog", 790 | user_font_size="18sp", 791 | on_release=self.show_settings, 792 | ) 793 | ) 794 | background_normal = os.path.join( 795 | self.data_dir, 796 | "images", 797 | "bg-field.png" 798 | if self.theme_cls.theme_style == "Light" 799 | else "bg-field-dark.png", 800 | ) 801 | self.instance_search_field = FileManagerTextFieldSearch( 802 | background_normal=background_normal, 803 | background_active=background_normal, 804 | manager=self, 805 | ) 806 | self.ids.header_box_menu.add_widget(Widget()) 807 | self.ids.header_box_menu.add_widget(self.instance_search_field) 808 | 809 | def update_background_search_field(self, instance, value): 810 | background_normal = os.path.join( 811 | self.data_dir, 812 | "images", 813 | "bg-field.png" if value == "Light" else "bg-field-dark.png", 814 | ) 815 | self.instance_search_field.background_normal = background_normal 816 | self.instance_search_field.background_active = background_normal 817 | 818 | def open_context_menu(self, entry_object, type_chooser): 819 | """Opens a context menu on right-clicking on a file or folder.""" 820 | 821 | menu = MDDropdownMenu( 822 | caller=entry_object, 823 | items=self.get_menu_right_click(entry_object, type_chooser), 824 | width_mult=4, 825 | background_color=self.theme_cls.bg_dark, 826 | max_height=dp(240), 827 | ) 828 | menu.bind( 829 | on_dismiss=self.context_menu_dismiss, 830 | ) 831 | menu.open() 832 | self.context_menu_open = True 833 | 834 | def tap_on_file_dir(self, *touch): 835 | """Called when the file/dir is clicked.""" 836 | 837 | type_click = touch[0][1].button 838 | # "FileChooserList" or "FileChooserIcon". 839 | type_chooser = touch[1] 840 | # FileThumbEntry object from file_chooser_icon.py file. 841 | entry_object = touch[0][0] 842 | 843 | if type_click == "right" and entry_object.path != "../": 844 | self.open_context_menu(entry_object, type_chooser) 845 | else: 846 | if entry_object.path == "../": 847 | entry_object.path = os.path.dirname(self.path) 848 | entry_object.collide_point( 849 | *touch[0][1].pos 850 | ) and self._instance_file_chooser_icon.entry_touched( 851 | entry_object, touch[0][1] 852 | ) 853 | if os.path.isdir(entry_object.path): 854 | self.set_path(entry_object.path) 855 | self.dispatch("on_tap_dir", entry_object.path) 856 | else: 857 | self.dispatch("on_tap_file", entry_object.path) 858 | if hasattr(entry_object, "remove_tooltip"): 859 | entry_object.remove_tooltip() 860 | 861 | def call_context_menu_plugin(self, name_plugin, entry_object): 862 | module = importlib.import_module( 863 | f"kivymd_extensions.filemanager.libs.plugins.contextmenu" 864 | ) 865 | plugin_cls = module.ContextMenuPlugin( 866 | instance_manager=self, 867 | entry_object=entry_object, 868 | ) 869 | plugin_cls.main(name_plugin) 870 | 871 | def tap_to_context_menu_item(self, text_item, entry_object): 872 | """ 873 | :type entry_object: 874 | :type instance_item: 875 | :type instance_menu: 876 | """ 877 | 878 | for data_item in self.menu_right_click_items: 879 | if ( 880 | list(data_item.items()) 881 | and text_item in list(data_item.items())[0] 882 | ): 883 | if "cls" in data_item: 884 | self.call_context_menu_plugin( 885 | data_item["cls"], entry_object 886 | ) 887 | break 888 | 889 | if text_item == "Open in new tab": 890 | self.add_tab(entry_object.path) 891 | self.dismiss_context_menu() 892 | 893 | def dismiss_context_menu(self): 894 | if self.context_menu_open: 895 | for widget in Window.children: 896 | if isinstance(widget, MDDropdownMenu): 897 | widget.dismiss() 898 | break 899 | 900 | def context_menu_dismiss(self, *args): 901 | """Called when closing the context menu.""" 902 | 903 | self.context_menu_open = False 904 | 905 | def get_menu_right_click(self, entry_object, type_chooser): 906 | """Returns a list of dictionaries for creating context menu items.""" 907 | 908 | menu_right_click_items = [] 909 | if type_chooser == "FileChooserIcon": 910 | for data_item in self.menu_right_click_items: 911 | if data_item: 912 | icon = list(data_item.items())[0][0] 913 | if icon: 914 | viewclass = "FileManagerItem" 915 | _txt_left_pad = dp(72) 916 | else: 917 | viewclass = "OneLineListItem" 918 | _txt_left_pad = dp(32) 919 | text = list(data_item.items())[0][1] 920 | menu_right_click_items.append( 921 | { 922 | "text": text, 923 | "viewclass": viewclass, 924 | "icon": icon, 925 | "font_style": "Caption", 926 | "height": dp(36), 927 | "top_pad": dp(4), 928 | "bot_pad": dp(10), 929 | "divider": None, 930 | "_txt_left_pad": _txt_left_pad, 931 | "on_release": lambda x=text, y=entry_object: self.tap_to_context_menu_item( 932 | x, y 933 | ), 934 | } 935 | ) 936 | if type_chooser == "FileChooserList": 937 | menu_right_click_items.append( 938 | { 939 | "viewclass": "OneLineListItem", 940 | "text": "Open in new tab", 941 | "font_style": "Caption", 942 | "height": dp(36), 943 | "divider": None, 944 | "top_pad": dp(4), 945 | "bot_pad": dp(10), 946 | "on_release": lambda x="Open in new tab", y=entry_object: self.tap_to_context_menu_item( 947 | x, y 948 | ), 949 | } 950 | ) 951 | return menu_right_click_items 952 | 953 | def get_icon_file(self, path_to_file): 954 | """Method that returns the icon path for the file.""" 955 | 956 | return self.ext_files.get( 957 | os.path.splitext(path_to_file)[1].replace(".", ""), 958 | os.path.join(self.path_to_skin, "file.png") 959 | if self.path_to_skin 960 | else "file-outline", 961 | ) 962 | 963 | def update_files(self, instance_pludin_dialog, path): 964 | # FIXME: Unable to update directory. You have to go to a higher level 965 | # and go back. 966 | self.set_path(os.getcwd()) 967 | self.set_path(os.path.dirname(path)) 968 | 969 | def is_dir(self, directory, filename): 970 | return os.path.isdir(os.path.join(directory, filename)) 971 | 972 | def on_tap_file(self, *args): 973 | """Called when the file is clicked.""" 974 | 975 | def on_tap_dir(self, *args): 976 | """Called when the folder is clicked.""" 977 | 978 | def on_tab_switch(self, *args): 979 | """Called when switching tab.""" 980 | 981 | def on_open_plugin_dialog(self, *args): 982 | self.dialog_plugin_open = True 983 | 984 | def on_dismiss_plugin_dialog(self, *args): 985 | self.dialog_plugin_open = False 986 | 987 | def on_context_menu(self, *args): 988 | """ 989 | Called at the end of any actions of the context menu, be it copying, 990 | archiving files and other actions. 991 | """ 992 | 993 | def on_open(self): 994 | """Called when the ModalView is opened.""" 995 | 996 | self.add_tab(self.path) 997 | self.create_header_menu() 998 | self.apply_palette() 999 | self.add_color_panel() 1000 | 1001 | def _on_tab_switch( 1002 | self, instance_tabs, instance_tab, instance_tab_label, tab_text 1003 | ): 1004 | self.current_open_tab_manager = instance_tab 1005 | self.dispatch( 1006 | "on_tab_switch", 1007 | instance_tabs, 1008 | instance_tab, 1009 | instance_tab_label, 1010 | tab_text, 1011 | ) 1012 | 1013 | def _set_state_close_theme_panel(self, *args): 1014 | self.settings_theme_panel_open = False 1015 | --------------------------------------------------------------------------------