from __future__ import annotations
from qtpy.QtWidgets import (QWidget, QFileSystemModel, QTreeView, QMenu, QAction, QGridLayout, QLineEdit, QPushButton,
QFileDialog, QHBoxLayout, QLabel)
from qtpy.QtCore import Qt, QModelIndex
import os
import datetime
import napari
try:
import pyperclip
except ImportError:
pass
from .table import read_csv
from .textedit import read_txt
from ..utils import viewer_imread, add_labeledarray
from ...core import imread
[docs]class Explorer(QWidget):
"""
A Read-only explorer widget. Capable of filter, set working directory, copy path and open file in the viewer.
By default QTreeView supports real time update on file change.
"""
def __init__(self, viewer:"napari.Viewer", path:str=""):
super().__init__(viewer.window._qt_window)
self.viewer = viewer
self.setLayout(QGridLayout())
self._add_change_root()
self._add_filetree(path)
self._add_filter_line()
def _add_change_root(self):
self.root_button = QPushButton(self)
self.root_button.setText("Change root directory")
@self.root_button.clicked.connect
def _():
dlg = QFileDialog()
dlg.setFileMode(QFileDialog.DirectoryOnly)
dlg.setHistory([self.tree.rootpath])
dirname = dlg.getExistingDirectory(self, caption="Select root ...", directory=self.tree.rootpath)
if dirname:
self.tree.rootpath = dirname
self.tree._set_file_model(dirname)
napari.utils.history.update_open_history(dirname)
return None
self.layout().addWidget(self.root_button)
return None
def _add_filetree(self, path:str=""):
"""
Add tree view of files with root directory set to ``path``.
Parameters
----------
path : str, default is ""
Path of the root directory. If not found, current directory will be used.
"""
path = os.getcwd() if not os.path.exists(path) else path
self.tree = FileTree(self, path)
self.layout().addWidget(self.tree)
return None
def _add_filter_line(self):
"""
Add line edit widget which filters file tree by file names.
"""
wid = QWidget(self)
wid.setLayout(QHBoxLayout())
# add label
label = QLabel(self)
label.setText("Filter file name: ")
self.line = QLineEdit(self)
self.line.setToolTip("Filter by names split by comma. e.g. '*.tif, *csv'.")
@self.line.editingFinished.connect
def _():
self.tree.set_filter(self.line.text())
wid.layout().addWidget(label)
wid.layout().addWidget(self.line)
self.layout().addWidget(wid)
[docs]class FileTree(QTreeView):
def __init__(self, parent, path:str):
super().__init__(parent=parent)
self.viewer = parent.viewer
self.rootpath = path
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.rightClickContextMenu)
self.setUniformRowHeights(True)
self.header().hide()
# Set QFileSystemModel
self.file_system = FileSystemModel(self)
self.file_system.setReadOnly(True)
self.file_system.setNameFilterDisables(False)
self._set_file_model(path)
# hide columns except for name
for i in range(1, self.file_system.columnCount()):
self.hideColumn(i)
self.doubleClicked.connect(self.onDoubleClicked)
self.show()
def _set_file_model(self, path:str):
self.file_system.setRootPath(path)
self.setModel(self.file_system)
self.setRootIndex(self.file_system.index(path))
return None
[docs] def open_path_at(self, index:QModelIndex):
path = self.file_system.filePath(index)
if os.path.isdir(path):
img = imread(os.path.join(path, "*.tif"))
add_labeledarray(self.viewer, img)
return None
_, ext = os.path.splitext(path)
if ext in (".tif", ".tiff", ".mrc", ".rec", ".png", ".jpg"):
viewer_imread(self.viewer, path)
elif ext in (".csv", ".dat"):
read_csv(self.viewer, path)
elif ext in (".txt",):
read_txt(self.viewer, path)
return None
[docs] def copy_path_at(self, index:QModelIndex):
"""
Copy the absolute path of the file at index. Double quotations are included.
"""
path = self.file_system.filePath(index)
pyperclip.copy('"' + path + '"')
return None
[docs] def onDoubleClicked(self, index:QModelIndex):
path = self.file_system.filePath(index)
if os.path.isfile(path):
self.open_path_at(index)
else:
return None
[docs] def set_filter(self, names:str|list[str]):
"""
Apply filter with comma separated string or list of string as an input.
"""
if isinstance(names, str):
if names == "":
names = "*"
names = names.split(",")
names = [s.strip() for s in names]
self.file_system.setNameFilters(names)
return None
[docs] def keyPressEvent(self, event):
if event.key() == Qt.Key_Return:
index = self.selected
index is None or self.onDoubleClicked(index)
elif event.key() == Qt.Key_C and event.modifiers() == Qt.ControlModifier:
index = self.selected
index is None or self.copy_path_at(index)
else:
return super().keyPressEvent(event)
@property
def selected(self) -> QModelIndex:
inds = self.selectionModel().selectedIndexes()
if len(inds) > 0:
index = inds[0]
else:
index = None
return index
[docs]class FileSystemModel(QFileSystemModel):
"""
File system model with tooltips.
"""
[docs] def data(self, index:QModelIndex, role:int=Qt.DisplayRole):
if role == Qt.ToolTipRole:
path = self.filePath(index)
name = os.path.basename(path)
stat = os.stat(path)
if os.path.isdir(path):
size = ""
elif stat.st_size < 10**3:
size = f"size: {stat.st_size:.1f} B\n"
elif stat.st_size < 10**6:
size = f"size: {stat.st_size/10**3:.1f} KB\n"
elif stat.st_size < 10**9:
size = f"size: {stat.st_size/10**6:.1f} MB\n"
else:
size = f"size: {stat.st_size/10**9:.1f} GB\n"
info = f"name: {name}\n" \
f"{size}" \
f"last accessed: {get_time_stamp(stat.st_atime)}\n" \
f"last modified: {get_time_stamp(stat.st_mtime)}"
return info
else:
return super().data(index, role)
[docs]def get_time_stamp(epoch):
t = str(datetime.datetime.fromtimestamp(epoch))
return t.split(".")[0]