33from PySide6 .QtWidgets import (
44 QWidget , QVBoxLayout , QHBoxLayout , QLabel , QListWidget ,
55 QPushButton , QFileDialog , QTextEdit , QStackedWidget ,
6- QSizePolicy
6+ QTreeWidget , QTreeWidgetItem , QHeaderView
77)
88from PySide6 .QtCore import Signal , Qt
99
1010from .models import AppSettings
1111
1212
1313class 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 )
0 commit comments