Coverage for main/components/file_selector_widget.py: 100%

81 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-03 15:51 +0000

1import os 

2 

3from PyQt6.QtGui import QIcon 

4from PyQt6.QtWidgets import ( 

5 QWidget, QHBoxLayout, QVBoxLayout, QListWidget, QPushButton, QListWidgetItem 

6) 

7from PyQt6.QtCore import Qt, pyqtSignal, QCoreApplication 

8 

9from components.nifti_file_dialog import NiftiFileDialog 

10 

11 

12class FileSelectorWidget(QWidget): 

13 """ 

14 Widget for selecting and displaying one or multiple NIfTI files. 

15 Provides a graphical list of selected files and buttons for choosing or clearing them. 

16 """ 

17 

18 # Signal emitted when the widget has at least one file selected (True/False) 

19 has_file = pyqtSignal(bool) 

20 

21 def __init__(self, context, has_existing_function, label, allow_multiple, 

22 processing=None, forced_filters=None, parent=None): 

23 """ 

24 Initialize the FileSelectorWidget. 

25 

26 Args: 

27 context (dict): Shared context containing signals and shared UI functions. 

28 has_existing_function (callable): Function to check if a file already exists. 

29 label (str): Label describing the purpose of the file selection. 

30 allow_multiple (bool): Whether multiple file selection is allowed. 

31 processing (pyqtSignal, optional): Signal controlling whether buttons are active. 

32 forced_filters (list[str], optional): File type filters. 

33 parent (QWidget, optional): Parent widget. 

34 """ 

35 super().__init__(parent) 

36 

37 self.context = context 

38 # Connects external signal for when files are selected from a shared dialog 

39 if self.context and "selected_files_signal" in self.context: 

40 self.context["selected_files_signal"].connect(self.set_selected_files) 

41 

42 self.label = label 

43 self.allow_multiple = allow_multiple 

44 self.has_existing_function = has_existing_function 

45 if processing: 

46 processing.connect(self.set_processing_mode) 

47 self.forced_filters = forced_filters 

48 

49 self.selected_files = None 

50 

51 # --- Main Layout --- 

52 file_selector_layout = QHBoxLayout(self) 

53 self.setLayout(file_selector_layout) 

54 

55 # --- File list display --- 

56 self.file_list_widget = QListWidget() 

57 self.file_list_widget.setSelectionMode(QListWidget.SelectionMode.NoSelection) 

58 self.file_list_widget.setFocusPolicy(Qt.FocusPolicy.NoFocus) 

59 self.file_list_widget.setMaximumHeight(100) 

60 file_selector_layout.addWidget(self.file_list_widget, stretch=1) 

61 

62 # --- Button container (Choose / Clear) --- 

63 button_container = QWidget() 

64 button_layout = QVBoxLayout(button_container) 

65 button_layout.addStretch() 

66 

67 # "Choose file(s)" button — opens NIfTI selection dialog 

68 self.file_button = QPushButton("Choose NIfTI File(s)") 

69 self.file_button.clicked.connect(self.open_tree_dialog) 

70 button_layout.addWidget(self.file_button, alignment=Qt.AlignmentFlag.AlignCenter) 

71 

72 # "Clear selection" button — removes all selected files 

73 self.clear_button = QPushButton("Clear Selection") 

74 self.clear_button.setEnabled(False) 

75 self.clear_button.clicked.connect(self.clear_selected_files) 

76 button_layout.addWidget(self.clear_button, alignment=Qt.AlignmentFlag.AlignCenter) 

77 

78 button_layout.addStretch() 

79 file_selector_layout.addWidget(button_container) 

80 

81 # --- Translation support --- 

82 self._translate_ui() 

83 if context and "language_changed" in context: 

84 context["language_changed"].connect(self._translate_ui) 

85 

86 # ------------------------------------------------------------------------- 

87 # Dialog and file selection 

88 # ------------------------------------------------------------------------- 

89 def open_tree_dialog(self): 

90 """ 

91 Opens the NIfTI file selection dialog (tree-based). 

92 Updates the file list upon user confirmation. 

93 """ 

94 results = NiftiFileDialog.get_files( 

95 context=self.context, 

96 allow_multiple=self.allow_multiple, 

97 has_existing_func=self.has_existing_function, 

98 label=self.label, 

99 forced_filters=self.forced_filters 

100 ) 

101 

102 if results: 

103 if self.allow_multiple: 

104 self.set_selected_files(results) 

105 else: 

106 # If single selection only, use the last selected file 

107 self.set_selected_files([results[0]]) 

108 

109 def clear_selected_files(self): 

110 """ 

111 Clears all selected files and updates UI state. 

112 """ 

113 self.selected_files = [] 

114 self.file_list_widget.clear() 

115 self.clear_button.setEnabled(False) 

116 self.has_file.emit(False) 

117 

118 # Notify the main UI to update its state 

119 if self.context and "update_main_buttons" in self.context: 

120 self.context["update_main_buttons"]() 

121 

122 def set_selected_files(self, file_paths): 

123 """ 

124 Updates the internal file list and UI after user selection. 

125 

126 Args: 

127 file_paths (list[str]): List of selected file paths. 

128 """ 

129 if self.allow_multiple: 

130 # Keep only valid NIfTI files (exclude folders and invalid extensions) 

131 file_paths = [ 

132 file for file in file_paths 

133 if os.path.exists(file) 

134 and not os.path.isdir(file) 

135 and (file.endswith('.nii.gz') or file.endswith('.nii')) 

136 ] 

137 else: 

138 # If only one file is allowed, keep the last one 

139 file_paths = [file_paths[-1]] 

140 

141 self.selected_files = file_paths 

142 self.file_list_widget.clear() 

143 

144 # Populate the list widget with file names 

145 for path in file_paths: 

146 item = QListWidgetItem(QIcon.fromTheme("document"), os.path.basename(path)) 

147 item.setToolTip(path) 

148 self.file_list_widget.addItem(item) 

149 

150 # Enable/disable buttons based on selection 

151 self.clear_button.setEnabled(bool(file_paths)) 

152 self.has_file.emit(bool(file_paths)) 

153 

154 # Notify main UI of state change 

155 if self.context and "update_main_buttons" in self.context: 

156 self.context["update_main_buttons"]() 

157 

158 # ------------------------------------------------------------------------- 

159 # Accessors and state management 

160 # ------------------------------------------------------------------------- 

161 def get_selected_files(self): 

162 """Return the list of selected file paths.""" 

163 return self.selected_files 

164 

165 def set_processing_mode(self, processing): 

166 """ 

167 Enable/disable user interaction when the app is processing data. 

168 

169 Args: 

170 processing (bool): True to disable file selection during processing. 

171 """ 

172 self.file_button.setEnabled(not processing) 

173 self.clear_button.setEnabled(not processing and bool(self.selected_files)) 

174 if processing: 

175 self.context["selected_files_signal"].disconnect() 

176 else: 

177 self.context["selected_files_signal"].connect(self.set_selected_files) 

178 

179 # ------------------------------------------------------------------------- 

180 # Translation 

181 # ------------------------------------------------------------------------- 

182 def _translate_ui(self): 

183 """Apply dynamic UI translations (for multi-language support).""" 

184 if self.allow_multiple: 

185 self.file_button.setText(QCoreApplication.translate("Components", "Choose NIfTI File(s)")) 

186 else: 

187 self.file_button.setText(QCoreApplication.translate("Components", "Choose NIfTI File")) 

188 self.clear_button.setText(QCoreApplication.translate("Components", "Clear Selection"))