From 161c1570bdf3cfc20fdf6025ac1f005c16fb8115 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Thu, 24 Jul 2025 17:22:53 +0200 Subject: [PATCH] wip - feat(file browser): add file browser widget --- bec_widgets/cli/client.py | 11 + .../utility/file_browser/file_browser.py | 298 ++++++++++++++++++ .../file_browser/file_browser.pyproject | 1 + .../file_browser/file_browser_plugin.py | 54 ++++ .../file_browser/register_file_browser.py | 15 + 5 files changed, 379 insertions(+) create mode 100644 bec_widgets/widgets/utility/file_browser/file_browser.py create mode 100644 bec_widgets/widgets/utility/file_browser/file_browser.pyproject create mode 100644 bec_widgets/widgets/utility/file_browser/file_browser_plugin.py create mode 100644 bec_widgets/widgets/utility/file_browser/register_file_browser.py diff --git a/bec_widgets/cli/client.py b/bec_widgets/cli/client.py index a0462ccb..f68ff281 100644 --- a/bec_widgets/cli/client.py +++ b/bec_widgets/cli/client.py @@ -37,6 +37,7 @@ _Widgets = { "DeviceBrowser": "DeviceBrowser", "DeviceComboBox": "DeviceComboBox", "DeviceLineEdit": "DeviceLineEdit", + "FileBrowser": "FileBrowser", "Heatmap": "Heatmap", "Image": "Image", "LogPanel": "LogPanel", @@ -1183,6 +1184,16 @@ class EllipticalROI(RPCBase): """ +class FileBrowser(RPCBase): + """A simple file browser widget.""" + + @rpc_call + def remove(self): + """ + Cleanup the BECConnector + """ + + class Heatmap(RPCBase): """Heatmap widget for visualizing 2d grid data with color mapping for the z-axis.""" diff --git a/bec_widgets/widgets/utility/file_browser/file_browser.py b/bec_widgets/widgets/utility/file_browser/file_browser.py new file mode 100644 index 00000000..cf95ea6c --- /dev/null +++ b/bec_widgets/widgets/utility/file_browser/file_browser.py @@ -0,0 +1,298 @@ +from PySide6.QtWidgets import QVBoxLayout +from qtpy.QtCore import QDir +from qtpy.QtWidgets import QApplication, QFileSystemModel, QHeaderView, QTreeView, QWidget + +from bec_widgets.utils.bec_widget import BECWidget +from bec_widgets.utils.error_popups import SafeProperty +from bec_widgets.utils.toolbars.actions import MaterialIconAction +from bec_widgets.utils.toolbars.bundles import ToolbarBundle +from bec_widgets.utils.toolbars.toolbar import ModularToolBar + + +class FileBrowser(BECWidget, QWidget): + """ + A simple file browser widget. + """ + + PLUGIN = True + ICON_NAME = "folder_open" + + def __init__(self, parent=None, config=None, client=None, gui_id=None, **kwargs): + super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs) + + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + self.tree = QTreeView(self) + + self.model = QFileSystemModel() + self.tree.setModel(self.model) + self.tree.setRootIndex(self.model.index(QDir.rootPath())) + self.model.setRootPath(QDir.rootPath()) + + self._allow_changing_root = True + self._original_root_path = QDir.rootPath() + + self.toolbar = ModularToolBar(parent=self, orientation="horizontal") + + self._go_back_action = MaterialIconAction("arrow_back", "Go Back", parent=self) + self.toolbar.components.add_safe("go_back", self._go_back_action) + self.toolbar.components.add_safe( + "refresh", MaterialIconAction("refresh", "Refresh", parent=self) + ) + self.toolbar.components.add_safe( + "open", MaterialIconAction("folder_open", "Open File", parent=self) + ) + bundle = ToolbarBundle("file_io", self.toolbar.components) + bundle.add_action("go_back") + bundle.add_action("refresh") + bundle.add_action("open") + self.toolbar.add_bundle(bundle) + + self.toolbar.show_bundles(["file_io"]) + + layout.addWidget(self.toolbar) + layout.addWidget(self.tree) + self.setLayout(layout) + + self._show_hidden_files = False + self._go_back_action.action.setEnabled(False) + self._go_back_action.action.triggered.connect(self._on_go_back) + + self.tree.setSelectionMode(QTreeView.SelectionMode.SingleSelection) + self.tree.doubleClicked.connect(self._on_double_click) + + def _on_double_click(self, index): + """ + Handle double-click events on the file browser. + Opens the selected file or directory. + """ + if not index.isValid(): + return + + path = self.model.filePath(index) + if self.model.isDir(index) and self._allow_changing_root: + self.tree.setRootIndex(index) + if path != self._original_root_path: + self._go_back_action.action.setEnabled(True) + else: + self._go_back_action.action.setEnabled(False) + return + print(f"Opening file: {path}") + + def _on_go_back(self): + """ + Handle the go back action. + Navigates to the previous directory in the file browser. + """ + if self._allow_changing_root and self.tree.rootIndex().isValid(): + parent_index = self.tree.rootIndex().parent() + if parent_index.isValid(): + self.tree.setRootIndex(parent_index) + if parent_index != self.model.index(self._original_root_path): + self._go_back_action.action.setEnabled(True) + else: + self._go_back_action.action.setEnabled(False) + + @SafeProperty(bool) + def show_toolbar(self): + """ + Get whether the toolbar is shown in the file browser. + """ + return not self.toolbar.isHidden() + + @show_toolbar.setter + def show_toolbar(self, show: bool): + """ + Set whether the toolbar is shown in the file browser. + """ + self.toolbar.setVisible(show) + + @SafeProperty(bool) + def show_hidden_files(self): + """ + Get whether hidden files are shown in the file browser. + """ + return self._show_hidden_files + + @show_hidden_files.setter + def show_hidden_files(self, show: bool): + """ + Set whether hidden files are shown in the file browser. + """ + self._show_hidden_files = show + if show: + self.model.setFilter( + QDir.Filter.AllDirs + | QDir.Filter.Files + | QDir.Filter.NoDotAndDotDot + | QDir.Filter.Hidden + ) + else: + self.model.setFilter( + QDir.Filter.AllDirs | QDir.Filter.Files | QDir.Filter.NoDotAndDotDot + ) + self.tree.setRootIndex(self.model.index(self.model.rootPath())) + + @SafeProperty(bool) + def allow_changing_root(self): + """ + Get whether changing the root path is allowed. + """ + return self._allow_changing_root + + @allow_changing_root.setter + def allow_changing_root(self, allow: bool): + """ + Set whether changing the root path is allowed. + """ + self._allow_changing_root = allow + + @SafeProperty(bool) + def show_file_size(self): + """ + Get whether the file size is shown in the file browser. + """ + index = self._section_index("Size") + return not self.tree.header().isSectionHidden(index) + + @show_file_size.setter + def show_file_size(self, show: bool): + """ + Set whether the file size is shown in the file browser. + """ + index = self._section_index("Size") + self.tree.header().setSectionHidden(index, not show) + self.tree.header().repaint() + + @SafeProperty(bool) + def show_file_kind(self): + """ + Get whether the file kind is shown in the file browser. + """ + index = self._section_index("Kind") + return not self.tree.header().isSectionHidden(index) + + @show_file_kind.setter + def show_file_kind(self, show: bool): + """ + Set whether the file kind is shown in the file browser. + """ + index = self._section_index("Kind") + self.tree.header().setSectionHidden(index, not show) + self.tree.setRootIndex(self.model.index(self.model.rootPath())) + + @SafeProperty(bool) + def show_file_timestamp(self): + """ + Get whether the file timestamp is shown in the file browser. + """ + index = self._section_index("Date Modified") + return not self.tree.header().isSectionHidden(index) + + @show_file_timestamp.setter + def show_file_timestamp(self, show: bool): + """ + Set whether the file timestamp is shown in the file browser. + """ + index = self._section_index("Date Modified") + self.tree.header().setSectionHidden(index, not show) + self.tree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) + self.tree.setRootIndex(self.model.index(self.model.rootPath())) + + @SafeProperty(str) + def root_path(self): + """ + Get the root path of the file browser. + """ + return self.model.rootPath() + + @root_path.setter + def root_path(self, path: str): + """ + Set the root path of the file browser. + """ + self.model.setRootPath(path) + self.tree.setRootIndex(self.model.index(path)) + self._original_root_path = path + + @SafeProperty(bool) + def show_header(self): + """ + Get whether the header is shown in the file browser. + """ + return not self.tree.header().isHidden() + + @show_header.setter + def show_header(self, show: bool): + """ + Set whether the header is shown in the file browser. + """ + self.tree.setHeaderHidden(not show) + self.tree.setRootIndex(self.model.index(self.model.rootPath())) + + def _section_index(self, label: str) -> int: + header = self.tree.header() + model = self.tree.model() + for i in range(model.columnCount()): + if model.headerData(i, header.orientation()) == label: + return i + print(f"Section '{label}' not found in header.") + return -1 + + +if __name__ == "__main__": + import sys + + app = QApplication(sys.argv) + file_browser = FileBrowser() + file_browser.root_path = "/Users/wakonig_k/software/work/bec-widgets/bec_widgets" + file_browser.show_file_size = False + file_browser.show_file_kind = False + file_browser.show_file_timestamp = False + file_browser.show_hidden_files = True + file_browser.show_header = False + file_browser.show() + sys.exit(app.exec_()) +# from qtpy.QtCore import Qt +# from qtpy.QtWidgets import QDockWidget, QFileSystemModel, QTreeView + +# class ExplorerDock(QWidget): +# def __init__(self, cpath, themes): +# super().__init__() +# self._themes = themes +# self.setWindowTitle("Explorer") +# self.tree = QTreeView(self) +# self.model = QFileSystemModel() +# self.tree.setModel(self.model) +# self.tree.setRootIndex(self.model.index(cpath)) +# self.model.setRootPath(cpath) + +# layout = QVBoxLayout(self) +# layout.setContentsMargins(0, 0, 0, 0) +# layout.addWidget(self.tree) + +# app = QApplication([]) +# explorer = ExplorerDock( +# cpath="/Users/wakonig_k/software/work/csaxs_bec/csaxs_bec", themes={"sidebar_bg": "#2E2E2E"} +# ) +# explorer.show() +# app.exec_() + + +# # self.dock = QDockWidget("Explorer", self) +# # self.dock.setMinimumWidth(200) +# # self.dock.visibilityChanged.connect( +# # lambda visible: self.onExplorerDockVisibilityChanged(visible) +# # ) +# # self.dock.setAllowedAreas(Qt.DockWidgetArea.AllDockWidgetAreas) +# # tree_view = QTreeView() + +# # self.model = QFileSystemModel() +# # bg = self._themes["sidebar_bg"] +# # tree_view.setStyleSheet( +# # f"QTreeView {{background-color: {bg}; color: white; border: none; }}" +# # ) +# # tree_view.setModel(self.model) +# # tree_view.setRootIndex(self.model.index(cpath)) +# # self.model.setRootPath(cpath) diff --git a/bec_widgets/widgets/utility/file_browser/file_browser.pyproject b/bec_widgets/widgets/utility/file_browser/file_browser.pyproject new file mode 100644 index 00000000..da89ff1d --- /dev/null +++ b/bec_widgets/widgets/utility/file_browser/file_browser.pyproject @@ -0,0 +1 @@ +{'files': ['file_browser.py']} \ No newline at end of file diff --git a/bec_widgets/widgets/utility/file_browser/file_browser_plugin.py b/bec_widgets/widgets/utility/file_browser/file_browser_plugin.py new file mode 100644 index 00000000..9432b7d0 --- /dev/null +++ b/bec_widgets/widgets/utility/file_browser/file_browser_plugin.py @@ -0,0 +1,54 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from qtpy.QtDesigner import QDesignerCustomWidgetInterface + +from bec_widgets.utils.bec_designer import designer_material_icon +from bec_widgets.widgets.utility.file_browser.file_browser import FileBrowser + +DOM_XML = """ + + + + +""" + + +class FileBrowserPlugin(QDesignerCustomWidgetInterface): # pragma: no cover + def __init__(self): + super().__init__() + self._form_editor = None + + def createWidget(self, parent): + t = FileBrowser(parent) + return t + + def domXml(self): + return DOM_XML + + def group(self): + return "BEC Utils" + + def icon(self): + return designer_material_icon(FileBrowser.ICON_NAME) + + def includeFile(self): + return "file_browser" + + def initialize(self, form_editor): + self._form_editor = form_editor + + def isContainer(self): + return False + + def isInitialized(self): + return self._form_editor is not None + + def name(self): + return "FileBrowser" + + def toolTip(self): + return "" + + def whatsThis(self): + return self.toolTip() diff --git a/bec_widgets/widgets/utility/file_browser/register_file_browser.py b/bec_widgets/widgets/utility/file_browser/register_file_browser.py new file mode 100644 index 00000000..c3e0caff --- /dev/null +++ b/bec_widgets/widgets/utility/file_browser/register_file_browser.py @@ -0,0 +1,15 @@ +def main(): # pragma: no cover + from qtpy import PYSIDE6 + + if not PYSIDE6: + print("PYSIDE6 is not available in the environment. Cannot patch designer.") + return + from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection + + from bec_widgets.widgets.utility.file_browser.file_browser_plugin import FileBrowserPlugin + + QPyDesignerCustomWidgetCollection.addCustomWidget(FileBrowserPlugin()) + + +if __name__ == "__main__": # pragma: no cover + main()