diff --git a/CHANGELOG b/CHANGELOG
index da94681..5f4ef8a 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -4,6 +4,7 @@
- enh: handle PermissionError when building directory tree
- enh: identify existing target paths based on size quicker
- enh: correct progress display and remove unused code
+ - enh: display object count and size in tree views
- ref: migrate from pkg_resources to importlib.resources
- ref: unify input and output tree widget with one base class
0.5.2
diff --git a/mpl_data_cast/gui/main.py b/mpl_data_cast/gui/main.py
index 250971f..aa75513 100644
--- a/mpl_data_cast/gui/main.py
+++ b/mpl_data_cast/gui/main.py
@@ -173,6 +173,8 @@ def on_recipe_changed(self):
rec_cls = self.current_recipe
doc = rec_cls.__doc__.split("\n")[0]
self.label_recipe_descr.setText(f"*{doc}*")
+ self.widget_input.recipe = rec_cls
+ self.widget_output.recipe = rec_cls
class Callback:
diff --git a/mpl_data_cast/gui/widget_tree.py b/mpl_data_cast/gui/widget_tree.py
index 0b26eb5..db2d320 100644
--- a/mpl_data_cast/gui/widget_tree.py
+++ b/mpl_data_cast/gui/widget_tree.py
@@ -1,24 +1,117 @@
-from PyQt6 import QtWidgets, QtCore, uic
from importlib import resources
import pathlib
+import time
+import threading
+from typing import Literal
+
+from PyQt6 import QtWidgets, QtCore, uic
from ..path_tree import PathTree, list_items_in_tree
+from ..recipe import map_recipe_name_to_class
from ..util import is_dir_writable
+class TreeObjectCounter(threading.Thread):
+ """Thread running in the background, counting objects and their size"""
+ def __init__(self, *args, **kwargs):
+ super(TreeObjectCounter, self).__init__(*args, **kwargs)
+ self.daemon = True
+ self.recipe = None
+ self.path = None
+ self.num_objects = 0
+ self.size_objects = 0
+ self.must_break = False
+ self.is_counting = False
+
+ def run(self):
+ recipe = self.recipe
+ path = self.path
+ while True:
+ if self.must_break:
+ # user requested we quit
+ break
+ elif recipe is None or path is None:
+ # not initialized yet
+ recipe = self.recipe
+ path = self.path
+ elif (self.recipe is None
+ or self.path is None
+ or self.recipe != recipe
+ or self.path != path):
+ # reset
+ self.num_objects = 0
+ self.size_objects = 0
+ recipe = self.recipe
+ path = self.path
+ elif self.num_objects:
+ # already counted
+ pass
+ else:
+ # start crawling the directory tree
+ self.is_counting = True
+ try:
+ rcp = recipe(path, path)
+ except BaseException:
+ pass
+ else:
+ tree_iterator = rcp.get_raw_data_iterator()
+ while True:
+ # check whether we have to abort
+ if (self.must_break
+ or recipe != self.recipe or path != self.path):
+ self.num_objects = 0
+ break
+ try:
+ item = next(tree_iterator)
+ except StopIteration:
+ break
+ except BaseException:
+ # Windows might encounter PermissionError.
+ pass
+ else:
+ self.num_objects += 1
+ try:
+ self.size_objects += sum(
+ [it.stat().st_size for it in item])
+ except BaseException:
+ pass
+ self.is_counting = False
+ time.sleep(0.5)
+
+
class TreeWidget(QtWidgets.QWidget):
- def __init__(self, which="tree", *args, **kwargs):
+ def __init__(self,
+ which: Literal["input", "output"] = "input",
+ *args,
+ **kwargs):
"""Widget handling tree views for directories"""
super(TreeWidget, self).__init__(*args, **kwargs)
ref_ui = resources.files("mpl_data_cast.gui") / "widget_tree.ui"
with resources.as_file(ref_ui) as path_ui:
uic.loadUi(path_ui, self)
+ #: keep track of recipe
+ self._recipe = map_recipe_name_to_class("CatchAll")
+ #: whether we are "output" or "input"
self.which = which
+ #: whether the directory tree should be read-only
+ self.readonly = which == "input"
+ #: path of current working directory
+ self._path = None
+ #: tree data structure
+ self.p_tree = None
+
+ # tree spider counter daemon
+ self.tree_counter = TreeObjectCounter()
+ self.tree_counter.start()
+
+ # UI update function
+ self.tree_label_timer = QtCore.QTimer(self)
+ self.tree_label_timer.timeout.connect(self.on_update_object_count)
+ self.tree_label_timer.start(300)
+
self.groupBox.setTitle(self.which.capitalize())
self.pushButton_dir.setText(f"Select {self.which} directory")
- self.path = None
- self.p_tree = None
self.settings = QtCore.QSettings()
self.tree_depth_limit = int(self.settings.value(
"main/tree_depth_limit", 3))
@@ -29,6 +122,25 @@ def __init__(self, which="tree", *args, **kwargs):
self.lineEdit_dir.editingFinished.connect(
self.update_tree_dir_from_lineedit)
+ @property
+ def path(self):
+ return self._path
+
+ @path.setter
+ def path(self, path):
+ self._path = path
+ self.tree_counter.path = path
+
+ @property
+ def recipe(self):
+ """The current recipe we are working with"""
+ return self._recipe
+
+ @recipe.setter
+ def recipe(self, recipe):
+ self._recipe = recipe
+ self.tree_counter.recipe = recipe
+
@QtCore.pyqtSlot()
def on_task_select_tree_dir(self) -> None:
p = QtWidgets.QFileDialog.getExistingDirectory(
@@ -38,6 +150,18 @@ def on_task_select_tree_dir(self) -> None:
if p:
self.update_tree_dir(p)
+ @QtCore.pyqtSlot()
+ def on_update_object_count(self):
+ objects = self.tree_counter.num_objects
+ size = self.tree_counter.size_objects
+ size_str = human_size(size)
+ if self.tree_counter.is_counting:
+ label = f"counting {objects} objects ({size_str})"
+ else:
+ label = f"{objects} objects ({size_str})"
+ self.label_objects.setText(label)
+
+ @QtCore.pyqtSlot(object)
def dragEnterEvent(self, e) -> None:
"""Whether files are accepted"""
if e.mimeData().hasUrls():
@@ -45,6 +169,7 @@ def dragEnterEvent(self, e) -> None:
else:
e.ignore()
+ @QtCore.pyqtSlot(object)
def dropEvent(self, e) -> None:
"""Add dropped directory to treeview and lineedit."""
urls = e.mimeData().urls()
@@ -54,7 +179,6 @@ def dropEvent(self, e) -> None:
path_tree = pp
else:
path_tree = pp.parent
-
self.update_tree_dir(path_tree)
@QtCore.pyqtSlot()
@@ -64,6 +188,11 @@ def update_tree_dir_from_lineedit(self) -> None:
if tree_dir:
self.update_tree_dir(tree_dir)
+ @QtCore.pyqtSlot()
+ def update_object_count(self) -> None:
+ """Update `self.label_objects` with the counted events"""
+
+ @QtCore.pyqtSlot()
def update_tree_dir(self, tree_dir: str | pathlib.Path) -> None:
"""Checks if the tree directory as given by the user exists and
updates the lineEdit widget accordingly.
@@ -73,20 +202,21 @@ def update_tree_dir(self, tree_dir: str | pathlib.Path) -> None:
tree_dir: str or pathlib.Path
The directory for the tree.
"""
- if is_dir_writable(tree_dir):
- tree_dir = pathlib.Path(tree_dir)
- self.path = tree_dir
- self.lineEdit_dir.setText(str(tree_dir))
- self.update_tree()
- else:
+ if not self.readonly and not is_dir_writable(tree_dir):
msg_txt = f"The {self.which} directory '{tree_dir}' is not " \
- f"valid. Please select a different directory."
+ f"writable. Please select a different directory."
msg = QtWidgets.QMessageBox(self)
msg.setIcon(QtWidgets.QMessageBox.Icon.Warning)
msg.setText(msg_txt)
msg.setWindowTitle(f"{self.which.capitalize()} directory invalid")
msg.exec()
+ else:
+ tree_dir = pathlib.Path(tree_dir)
+ self.path = tree_dir
+ self.lineEdit_dir.setText(str(tree_dir))
+ self.update_tree()
+ @QtCore.pyqtSlot()
def update_tree(self) -> None:
"""Update the `PathTree` object based on the current root path in
`self.path` and update the GUI to show the new tree."""
@@ -101,3 +231,10 @@ def update_tree(self) -> None:
QtWidgets.QApplication.processEvents(
QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 300)
+
+
+def human_size(bt, units=None):
+ """Return a human-eadable string representation of bytes """
+ if units is None:
+ units = [' bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB']
+ return str(bt) + units[0] if bt < 1024 else human_size(bt >> 10, units[1:])
diff --git a/mpl_data_cast/gui/widget_tree.ui b/mpl_data_cast/gui/widget_tree.ui
index 8bf1ab9..bd48ab9 100644
--- a/mpl_data_cast/gui/widget_tree.ui
+++ b/mpl_data_cast/gui/widget_tree.ui
@@ -28,128 +28,110 @@
Input/Output
-
-
- 5
-
-
- 5
-
-
- 5
-
-
- 5
-
+
-
-
-
- 3
-
-
- 2
-
-
- 3
-
-
- 2
-
-
-
-
-
-
-
-
-
- 0
- 0
-
-
-
- false
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 120
- 0
-
-
-
- Select input/target directory
-
-
-
-
-
+
-
-
-
- true
-
-
- QFrame::Box
+
+
+
+ 0
+ 0
+
-
- QAbstractScrollArea::AdjustIgnored
-
-
+
false
-
- QAbstractItemView::NoDragDrop
-
-
- true
-
-
- QAbstractItemView::NoSelection
-
-
- QAbstractItemView::SelectRows
-
-
- Qt::ElideRight
-
-
- 5
-
-
- true
+
+
+ -
+
+
+
+ 0
+ 0
+
-
- 1
+
+
+ 120
+ 0
+
-
- 0
+
+ Select input/target directory
-
- true
-
-
- false
-
-
- 125
-
-
-
- 1
-
-
+ -
+
+
+ true
+
+
+ QFrame::Box
+
+
+ QAbstractScrollArea::AdjustIgnored
+
+
+ false
+
+
+ QAbstractItemView::NoDragDrop
+
+
+ true
+
+
+ QAbstractItemView::NoSelection
+
+
+ QAbstractItemView::SelectRows
+
+
+ Qt::ElideRight
+
+
+ 5
+
+
+ true
+
+
+ 1
+
+
+ 0
+
+
+ true
+
+
+ false
+
+
+ 125
+
+
+
+ 1
+
+
+
+
+ -
+
+
+ 0 objects
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+