Skip to content

Commit 4346775

Browse files
Add reference directory support for grouped scan tools
Each included directory now has a "Ref" checkbox in the bottom panel. When checked, the directory is marked as a reference — files in reference directories are kept and never selected for deletion. This matches the Krokiet (Slint) UI's reference path feature. Changes: - AppSettings: added reference_paths set, persisted in settings JSON - BottomPanel: replaced QListWidget with QTreeWidget showing [Ref][Path] columns; Ref checkbox toggles reference_paths membership - Backend: passes -r flag with reference directories for tools that support it (duplicates, similar images/videos/music) - Non-grouped tools (empty files, broken files, etc.) ignore -r Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent aead23b commit 4346775

4 files changed

Lines changed: 106 additions & 39 deletions

File tree

czkawka_pyside6/app/backend.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,15 @@ def _build_command(self) -> list[str]:
187187
if s.thread_number > 0:
188188
cmd.extend(["-T", str(s.thread_number)])
189189

190+
# Reference directories (only for grouped tools that support -r)
191+
if s.reference_paths and self.tab in (
192+
ActiveTab.DUPLICATE_FILES, ActiveTab.SIMILAR_IMAGES,
193+
ActiveTab.SIMILAR_VIDEOS, ActiveTab.SIMILAR_MUSIC,
194+
):
195+
ref_dirs = [p for p in s.reference_paths if p in s.included_paths]
196+
if ref_dirs:
197+
cmd.extend(["-r", ",".join(ref_dirs)])
198+
190199
# Tool-specific args
191200
if self.tab == ActiveTab.DUPLICATE_FILES:
192201
cmd.extend(["-s", ts.dup_check_method.value])

czkawka_pyside6/app/bottom_panel.py

Lines changed: 94 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,21 @@
33
from PySide6.QtWidgets import (
44
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QListWidget,
55
QPushButton, QFileDialog, QTextEdit, QStackedWidget,
6-
QSizePolicy
6+
QTreeWidget, QTreeWidgetItem, QHeaderView
77
)
88
from PySide6.QtCore import Signal, Qt
99

1010
from .models import AppSettings
1111

1212

1313
class BottomPanel(QWidget):
14-
"""Bottom panel showing directories or error messages."""
14+
"""Bottom panel showing directories or error messages.
15+
16+
Included directories have a "Ref" checkbox — when checked, that
17+
directory is a reference directory: its files are kept as references
18+
and never selected for deletion in grouped tools (duplicates, similar
19+
images/videos/music).
20+
"""
1521
directories_changed = Signal()
1622

1723
def __init__(self, settings: AppSettings, parent=None):
@@ -32,32 +38,41 @@ def _setup_ui(self):
3238
dir_layout = QHBoxLayout(dir_widget)
3339
dir_layout.setContentsMargins(0, 0, 0, 0)
3440

35-
# Included directories
41+
# ── Included directories (with Ref checkbox) ──
3642
inc_widget = QWidget()
3743
inc_layout = QVBoxLayout(inc_widget)
3844
inc_layout.setContentsMargins(0, 0, 0, 0)
3945
inc_layout.addWidget(QLabel("Included Directories:"))
4046

41-
self._inc_list = QListWidget()
42-
self._inc_list.setMaximumHeight(120)
43-
for path in self._settings.included_paths:
44-
self._inc_list.addItem(path)
45-
inc_layout.addWidget(self._inc_list)
47+
self._inc_tree = QTreeWidget()
48+
self._inc_tree.setMaximumHeight(120)
49+
self._inc_tree.setHeaderLabels(["Ref", "Path"])
50+
self._inc_tree.setColumnCount(2)
51+
header = self._inc_tree.header()
52+
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
53+
header.setSectionResizeMode(1, QHeaderView.Stretch)
54+
self._inc_tree.setRootIsDecorated(False)
55+
self._inc_tree.itemChanged.connect(self._on_ref_toggled)
56+
57+
self._populate_included()
58+
inc_layout.addWidget(self._inc_tree)
4659

4760
inc_btns = QHBoxLayout()
4861
add_btn = QPushButton("+")
4962
add_btn.setFixedWidth(30)
63+
add_btn.setToolTip("Add included directory")
5064
add_btn.clicked.connect(self._add_included)
5165
inc_btns.addWidget(add_btn)
5266
rem_btn = QPushButton("-")
5367
rem_btn.setFixedWidth(30)
68+
rem_btn.setToolTip("Remove selected directory")
5469
rem_btn.clicked.connect(self._remove_included)
5570
inc_btns.addWidget(rem_btn)
5671
inc_btns.addStretch()
5772
inc_layout.addLayout(inc_btns)
5873
dir_layout.addWidget(inc_widget)
5974

60-
# Excluded directories
75+
# ── Excluded directories ──
6176
exc_widget = QWidget()
6277
exc_layout = QVBoxLayout(exc_widget)
6378
exc_layout.setContentsMargins(0, 0, 0, 0)
@@ -72,10 +87,12 @@ def _setup_ui(self):
7287
exc_btns = QHBoxLayout()
7388
add_exc = QPushButton("+")
7489
add_exc.setFixedWidth(30)
90+
add_exc.setToolTip("Add excluded directory")
7591
add_exc.clicked.connect(self._add_excluded)
7692
exc_btns.addWidget(add_exc)
7793
rem_exc = QPushButton("-")
7894
rem_exc.setFixedWidth(30)
95+
rem_exc.setToolTip("Remove selected directory")
7996
rem_exc.clicked.connect(self._remove_excluded)
8097
exc_btns.addWidget(rem_exc)
8198
exc_btns.addStretch()
@@ -92,36 +109,57 @@ def _setup_ui(self):
92109

93110
layout.addWidget(self._stack)
94111

95-
def show_directories(self):
96-
self._stack.setCurrentIndex(0)
97-
self.setVisible(True)
98-
99-
def show_text(self):
100-
self._stack.setCurrentIndex(1)
101-
self.setVisible(True)
102-
103-
def hide_panel(self):
104-
self.setVisible(False)
112+
# ── Included directory helpers ────────────────────────────
105113

106-
def set_text(self, text: str):
107-
self._text_area.setPlainText(text)
108-
109-
def append_text(self, text: str):
110-
self._text_area.append(text)
114+
def _populate_included(self):
115+
"""Rebuild the included paths tree from settings."""
116+
self._inc_tree.blockSignals(True)
117+
self._inc_tree.clear()
118+
for path in self._settings.included_paths:
119+
item = QTreeWidgetItem()
120+
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
121+
is_ref = path in self._settings.reference_paths
122+
item.setCheckState(0, Qt.Checked if is_ref else Qt.Unchecked)
123+
item.setText(1, path)
124+
item.setToolTip(0, "Check to mark as reference directory.\n"
125+
"Files in reference directories are never selected for deletion.")
126+
self._inc_tree.addTopLevelItem(item)
127+
self._inc_tree.blockSignals(False)
128+
129+
def _on_ref_toggled(self, item, column):
130+
"""Handle Ref checkbox toggle."""
131+
if column != 0:
132+
return
133+
path = item.text(1)
134+
if item.checkState(0) == Qt.Checked:
135+
self._settings.reference_paths.add(path)
136+
else:
137+
self._settings.reference_paths.discard(path)
138+
self.directories_changed.emit()
111139

112140
def _add_included(self):
113141
path = QFileDialog.getExistingDirectory(self, "Select Directory to Include")
114142
if path and path not in self._settings.included_paths:
115143
self._settings.included_paths.append(path)
116-
self._inc_list.addItem(path)
144+
self._populate_included()
117145
self.directories_changed.emit()
118146

119147
def _remove_included(self):
120-
row = self._inc_list.currentRow()
121-
if row >= 0:
122-
self._inc_list.takeItem(row)
123-
self._settings.included_paths.pop(row)
124-
self.directories_changed.emit()
148+
items = self._inc_tree.selectedItems()
149+
if not items:
150+
# Try current item
151+
item = self._inc_tree.currentItem()
152+
if item:
153+
items = [item]
154+
for item in items:
155+
path = item.text(1)
156+
if path in self._settings.included_paths:
157+
self._settings.included_paths.remove(path)
158+
self._settings.reference_paths.discard(path)
159+
self._populate_included()
160+
self.directories_changed.emit()
161+
162+
# ── Excluded directory helpers ────────────────────────────
125163

126164
def _add_excluded(self):
127165
path = QFileDialog.getExistingDirectory(self, "Select Directory to Exclude")
@@ -137,6 +175,31 @@ def _remove_excluded(self):
137175
self._settings.excluded_paths.pop(row)
138176
self.directories_changed.emit()
139177

178+
# ── Public API ────────────────────────────────────────────
179+
180+
def show_directories(self):
181+
self._stack.setCurrentIndex(0)
182+
self.setVisible(True)
183+
184+
def show_text(self):
185+
self._stack.setCurrentIndex(1)
186+
self.setVisible(True)
187+
188+
def hide_panel(self):
189+
self.setVisible(False)
190+
191+
def set_text(self, text: str):
192+
self._text_area.setPlainText(text)
193+
194+
def append_text(self, text: str):
195+
self._text_area.append(text)
196+
197+
def refresh_lists(self):
198+
self._populate_included()
199+
self._exc_list.clear()
200+
for path in self._settings.excluded_paths:
201+
self._exc_list.addItem(path)
202+
140203
def dragEnterEvent(self, event):
141204
if event.mimeData().hasUrls():
142205
event.acceptProposedAction()
@@ -147,13 +210,5 @@ def dropEvent(self, event):
147210
if path and os.path.isdir(path):
148211
if path not in self._settings.included_paths:
149212
self._settings.included_paths.append(path)
150-
self._inc_list.addItem(path)
213+
self._populate_included()
151214
self.directories_changed.emit()
152-
153-
def refresh_lists(self):
154-
self._inc_list.clear()
155-
for path in self._settings.included_paths:
156-
self._inc_list.addItem(path)
157-
self._exc_list.clear()
158-
for path in self._settings.excluded_paths:
159-
self._exc_list.addItem(path)

czkawka_pyside6/app/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ class ToolSettings:
288288
class AppSettings:
289289
"""Global application settings."""
290290
included_paths: list = field(default_factory=lambda: [str(Path.home())])
291+
reference_paths: set = field(default_factory=set) # subset of included_paths marked as reference
291292
excluded_paths: list = field(default_factory=list)
292293
excluded_items: str = ""
293294
allowed_extensions: str = ""

czkawka_pyside6/app/state.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ def save_settings(self):
7979
config_file = self._config_path / "settings.json"
8080
data = {
8181
"included_paths": self.settings.included_paths,
82+
"reference_paths": list(self.settings.reference_paths),
8283
"excluded_paths": self.settings.excluded_paths,
8384
"excluded_items": self.settings.excluded_items,
8485
"allowed_extensions": self.settings.allowed_extensions,
@@ -109,6 +110,7 @@ def load_settings(self):
109110
data = json.loads(config_file.read_text())
110111
s = self.settings
111112
s.included_paths = data.get("included_paths", s.included_paths)
113+
s.reference_paths = set(data.get("reference_paths", []))
112114
s.excluded_paths = data.get("excluded_paths", s.excluded_paths)
113115
s.excluded_items = data.get("excluded_items", s.excluded_items)
114116
s.allowed_extensions = data.get("allowed_extensions", s.allowed_extensions)

0 commit comments

Comments
 (0)