Coverage for vibefpmgui/complexity_panel.py: 91%

346 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-11-01 20:51 +0000

1"""The *Complexity* tab: each procedure's complexity, as lizard measures it 

2(see `vibefpmgui.complexity`): CCN, lines of code, tokens, parameters, 

3length and nesting depth, the values over lizard's thresholds in red; each 

4file's totals; and the selected procedure's control-flow graph (see 

5`vibefpmgui.control_flow`, `vibefpmgui.cfg_view`). Click a procedure to open 

6it; click a file to list only its procedures (its first one selected and 

7drawn); click a node of the graph to open its lines. 

8""" 

9 

10from __future__ import annotations 

11 

12import html 

13from pathlib import Path 

14from typing import NamedTuple 

15 

16from qtpy.QtCore import Qt, Signal 

17from qtpy.QtGui import QColor, QFont 

18from qtpy.QtWidgets import ( 

19 QAbstractItemView, 

20 QApplication, 

21 QCheckBox, 

22 QHBoxLayout, 

23 QHeaderView, 

24 QLabel, 

25 QLineEdit, 

26 QSplitter, 

27 QVBoxLayout, 

28 QWidget, 

29) 

30 

31from . import complexity as cx 

32from . import control_flow as cf 

33from .build_monitor import PALETTES, TitleIcon, app_is_light, panel_stylesheet 

34from .cfg_view import CfgView 

35from .icons import symbol_icon 

36from .metrics_table import Column, MetricsTable, RowStyle 

37from .source_text import read_source 

38from .widgets import StatusLabel 

39 

40OVER = "#e03131" # a value over its threshold 

41PROCEDURE_COLUMNS = ("FILE", "PROCEDURE", "CCN", "NLOC", "TOKENS", "PARAMS", "LENGTH", "DEPTH") 

42FILE_COLUMN, NAME_COLUMN = 0, 1 # (in the procedures' table) 

43MAX_TEXT_WIDTH = 320 # a file's or procedure's column at most (wider: drag it; a long path elided at its start) 

44FILE_COLUMNS = ("FILE", "NLOC", "PROCEDURES", "AVG CCN", "MAX CCN", "OVER") 

45COLUMN_TIPS = { 

46 "CCN": "Cyclomatic complexity: 1 + its decisions (if, else if, loops, case, type is, where, .and., .or.)", 

47 "NLOC": "Lines of code (not blank, not only a comment)", 

48 "TOKENS": "Tokens in its code", 

49 "PARAMS": "Dummy arguments", 

50 "LENGTH": "Lines, first to last (with comments and its internal procedures)", 

51 "DEPTH": "How deeply its block constructs (do, if…then, select…) nest", 

52 "OVER": "Procedures with a value over its threshold", 

53} 

54METRIC_OF_COLUMN = {"CCN": "ccn", "PARAMS": "params", "LENGTH": "length", "DEPTH": "depth"} 

55PROCEDURE_ROLE = Qt.UserRole + 1 # a procedure's row: its complexity.Procedure 

56 

57 

58class ProcedureRow(NamedTuple): 

59 file: cx.FileMetrics 

60 proc: cx.Procedure 

61 label: str # its file's label 

62 over: frozenset # its metrics over their thresholds ("ccn"…) 

63 

64 

65class FileRow(NamedTuple): 

66 file: cx.FileMetrics 

67 label: str 

68 over: int # its procedures over a threshold 

69 highest: int # its highest CCN 

70FILE_ROLE = Qt.UserRole + 2 # a row's file (its FileMetrics' path: a procedure in an included file's is the includer's) 

71 

72 

73class ComplexityPanel(QWidget): 

74 """The files (top), their procedures (below), their totals, and the 

75 selected procedure's control-flow graph (beside them).""" 

76 

77 open_requested = Signal(str, int) # file, line 

78 refresh_requested = Signal() 

79 

80 def __init__(self, parent=None): 

81 super().__init__(parent) 

82 self.files: list[cx.FileMetrics] = [] 

83 self._labels: dict[Path, str] = {} # (a file's label, worked out once: see label) 

84 self._is_test: dict[Path, bool] = {} 

85 self.project_dir: Path | None = None 

86 self.test_dirs: list[Path] = [] 

87 self.dependency_dirs: list[Path] = [] 

88 self.thresholds = dict(cx.THRESHOLDS) 

89 self.description = "" # (when it was measured) 

90 self.icon_for = None # a file's QIcon, as in Project files (set by the window) 

91 self.is_light = True 

92 self.palette_colors = PALETTES["light"] 

93 self.setObjectName("ComplexityPanel") 

94 self.setAttribute(Qt.WA_StyledBackground, True) 

95 

96 self.title_icon = TitleIcon("complexity") 

97 self.title = QLabel("Complexity") 

98 self.subtitle = StatusLabel() 

99 self.tests_check = QCheckBox("Include tests") 

100 self.tests_check.setToolTip("Also measure the test programs") 

101 self.tests_check.toggled.connect(self._fill) 

102 self.dependencies_check = QCheckBox("Include dependencies") 

103 self.dependencies_check.setToolTip("Also measure the dependencies' code") 

104 self.dependencies_check.toggled.connect(self.refresh_requested) 

105 header = QHBoxLayout() 

106 header.setSpacing(6) 

107 header.addWidget(self.title_icon) 

108 header.addWidget(self.title) 

109 header.addSpacing(4) 

110 header.addWidget(self.subtitle, 1) 

111 header.addWidget(self.tests_check) 

112 header.addSpacing(12) 

113 header.addWidget(self.dependencies_check) 

114 

115 self.filter_edit = QLineEdit() 

116 self.filter_edit.setPlaceholderText("Filter by procedure or file") 

117 self.filter_edit.setClearButtonEnabled(True) 

118 self.filter_edit.setMinimumWidth(160) # (narrow: the totals beside it are cut off, not the filter) 

119 self.filter_edit.textChanged.connect(self._fill_procedures) 

120 self.totals = StatusLabel() 

121 totals_row = QHBoxLayout() 

122 totals_row.setSpacing(10) 

123 totals_row.addWidget(self.filter_edit, 1) 

124 totals_row.addWidget(self.totals) 

125 

126 metric = {"CCN": "ccn", "NLOC": "nloc", "TOKENS": "tokens", "PARAMS": "params", "LENGTH": "length", 

127 "DEPTH": "depth"} 

128 procedure_columns = [Column("FILE", lambda r: r.label), Column("PROCEDURE", lambda r: r.proc.name)] + [ 

129 Column(name, lambda r, k=key: f"{getattr(r.proc, k):,}", lambda r, k=key: getattr(r.proc, k), 

130 COLUMN_TIPS.get(name, "")) for name, key in metric.items()] 

131 self.procedures = self._table(procedure_columns, RowStyle( 

132 color=self._procedure_color, bold=self._procedure_bold, icon=lambda r: symbol_icon(r.proc.kind), 

133 tip=lambda r: self._procedure_tip(r.proc, r.label, r.over), data=self._procedure_data, 

134 icon_column=NAME_COLUMN)) 

135 self.procedures.horizontalHeader().setSortIndicator(2, Qt.DescendingOrder) # (the most complex first) 

136 self.procedures.cellClicked.connect(self._procedure_clicked) 

137 file_columns = [ 

138 Column("FILE", lambda r: r.label), 

139 Column("NLOC", lambda r: f"{r.file.nloc:,}", lambda r: r.file.nloc, COLUMN_TIPS["NLOC"]), 

140 Column("PROCEDURES", lambda r: str(len(r.file.procedures)), lambda r: len(r.file.procedures)), 

141 Column("AVG CCN", lambda r: f"{r.file.average_ccn:.1f}", lambda r: r.file.average_ccn), 

142 Column("MAX CCN", lambda r: str(r.highest), lambda r: r.highest), 

143 Column("OVER", lambda r: str(r.over), lambda r: r.over, COLUMN_TIPS["OVER"])] 

144 self.file_table = self._table(file_columns, RowStyle( 

145 color=lambda r, column: QColor(OVER) if column == len(FILE_COLUMNS) - 1 and r.over else None, 

146 icon=lambda r: self.icon_for(r.file.path) if self.icon_for is not None else None, 

147 tip=lambda r: f"{html.escape(str(r.file.path))}<br>Click to list only its procedures", 

148 data=lambda r, role: r.label if role == Qt.UserRole else r.file.path if role == FILE_ROLE else None)) 

149 self.file_table.horizontalHeader().setSortIndicator(3, Qt.DescendingOrder) 

150 # A file chosen (clicked, or with the arrow keys): only its procedures; 

151 # the chosen file clicked again: all of them. 

152 self.file_table.currentCellChanged.connect(self._file_chosen) 

153 self.file_table.cellClicked.connect(self._file_clicked) 

154 self._file_just_chosen = False 

155 self.splitter = QSplitter(Qt.Vertical) # (the files above their procedures) 

156 self.splitter.addWidget(self.file_table) 

157 self.splitter.addWidget(self.procedures) 

158 self.splitter.setStretchFactor(0, 1) 

159 self.splitter.setStretchFactor(1, 3) 

160 # The selected procedure's control flow, beside the tables. 

161 self.flow_title = StatusLabel() 

162 self.flow = CfgView() 

163 self.flow.line_clicked.connect(self._flow_line_clicked) 

164 self.flow_shown: cx.Procedure | None = None 

165 flow_box = QWidget() 

166 flow_layout = QVBoxLayout(flow_box) 

167 flow_layout.setContentsMargins(0, 0, 0, 0) 

168 flow_layout.setSpacing(4) 

169 flow_layout.addWidget(self.flow_title) 

170 flow_layout.addWidget(self.flow, 1) 

171 self.body = QSplitter(Qt.Horizontal) 

172 self.body.addWidget(self.splitter) 

173 self.body.addWidget(flow_box) 

174 self.body.setStretchFactor(0, 3) 

175 self.body.setStretchFactor(1, 2) 

176 self.procedures.currentCellChanged.connect(lambda row, *_: self._show_flow(row)) 

177 

178 self.empty = QLabel() 

179 self.empty.setAlignment(Qt.AlignCenter) 

180 self.empty.setWordWrap(True) 

181 

182 layout = QVBoxLayout(self) 

183 layout.setContentsMargins(14, 10, 14, 6) 

184 layout.setSpacing(8) 

185 layout.addLayout(header) 

186 layout.addLayout(totals_row) 

187 layout.addWidget(self.body, 1) 

188 layout.addWidget(self.empty, 1) 

189 self.refresh_fonts() 

190 self.show_message("Open a project to see the complexity of its Fortran code here.") 

191 

192 def _table(self, columns: list[Column], style: RowStyle) -> MetricsTable: 

193 """A table whose columns are as wide as their contents, scrolling 

194 sideways if the panel is narrower (not squeezing the first column); 

195 its cells drawn on demand (see `vibefpmgui.metrics_table`).""" 

196 table = MetricsTable(columns, style) 

197 table.verticalHeader().setVisible(False) 

198 table.setShowGrid(False) 

199 table.setEditTriggers(QAbstractItemView.NoEditTriggers) 

200 table.setSelectionBehavior(QAbstractItemView.SelectRows) 

201 table.setSelectionMode(QAbstractItemView.SingleSelection) 

202 table.setSortingEnabled(True) 

203 table.setWordWrap(False) 

204 table.setTextElideMode(Qt.ElideLeft) # (a long path: the end of its file name still shows) 

205 table.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) 

206 hh = table.horizontalHeader() 

207 hh.setDefaultAlignment(Qt.AlignLeft | Qt.AlignVCenter) 

208 hh.setSectionResizeMode(QHeaderView.Interactive) 

209 hh.setStretchLastSection(False) 

210 hh.sortIndicatorChanged.connect(lambda *_: self._fit_columns(table)) 

211 return table 

212 

213 @staticmethod 

214 def _fit_columns(table: MetricsTable): 

215 """Each column as wide as its values and its title, room for the sort 

216 arrow only in the sorted one (Qt leaves room for it in every title).""" 

217 header = table.horizontalHeader() 

218 header.setSortIndicatorShown(False) 

219 table.resizeColumnsToContents() 

220 header.setSortIndicatorShown(True) 

221 column = header.sortIndicatorSection() 

222 if 0 <= column < header.count(): 222 ↛ 224line 222 didn't jump to line 224 because the condition on line 222 was always true

223 header.resizeSection(column, max(header.sectionSize(column), header.sectionSizeHint(column))) 

224 for column in range(header.count()): 

225 if header.sectionSize(column) > MAX_TEXT_WIDTH: 225 ↛ 226line 225 didn't jump to line 226 because the condition on line 225 was never true

226 header.resizeSection(column, MAX_TEXT_WIDTH) 

227 

228 # ------------------------------------------------------------ styling 

229 def refresh_palette(self): 

230 self.is_light = app_is_light() 

231 self.palette_colors = pal = PALETTES["light" if self.is_light else "dark"] 

232 self.setStyleSheet(panel_stylesheet(self.objectName(), pal) 

233 + f"#{self.objectName()} QHeaderView::section {{ padding: 6px 8px; }}") 

234 self.title_icon.draw(pal["text"], self.title) 

235 for label in (self.subtitle, self.empty, self.totals, self.flow_title): 

236 label.setStyleSheet(f"color: {pal['muted']};") 

237 self.flow.set_dark(not self.is_light) 

238 self._fill() 

239 

240 def refresh_fonts(self): 

241 font = QFont(QApplication.font()) 

242 font.setPointSizeF(font.pointSizeF() + 2) 

243 font.setWeight(QFont.Bold) 

244 self.title.setFont(font) 

245 self.refresh_palette() 

246 

247 # -------------------------------------------------------------- state 

248 def show_message(self, message: str): 

249 """Nothing to list: why, or what's happening.""" 

250 self.empty.setText(message) 

251 self.empty.show() 

252 self.body.hide() 

253 self.filter_edit.setEnabled(False) 

254 self.totals.setText("") 

255 

256 def show_metrics(self, files: list[cx.FileMetrics], project_dir: Path, test_dirs: list[Path] | None = None, 

257 dependency_dirs: list[Path] | None = None, description: str = ""): 

258 """Every measured file's metrics (the project's tests and the 

259 dependencies' are listed only with their boxes ticked).""" 

260 place = (project_dir.resolve(), [Path(d).resolve() for d in test_dirs or []], 

261 [Path(d).resolve() for d in dependency_dirs or []]) 

262 if place != (self.project_dir, self.test_dirs, self.dependency_dirs): 

263 self._labels.clear() 

264 self._is_test.clear() 

265 self.files = files 

266 self.project_dir, self.test_dirs, self.dependency_dirs = place 

267 self.description = description 

268 self._fill() 

269 

270 def shown_files(self) -> list[cx.FileMetrics]: 

271 """The files listed: the project's own, with its tests if ticked 

272 (the dependencies' are only measured when their box is ticked).""" 

273 if self.tests_check.isChecked(): 

274 return list(self.files) 

275 return [f for f in self.files if not self._test_file(f.path)] 

276 

277 def _test_file(self, path: Path) -> bool: 

278 found = self._is_test.get(path) 

279 if found is None: 

280 resolved = path.resolve() 

281 found = self._is_test[path] = any(resolved.is_relative_to(d) for d in self.test_dirs) 

282 return found 

283 

284 def label(self, path: Path) -> str: 

285 """A file's name as listed: its path in the project (or in its dependency).""" 

286 label = self._labels.get(path) 

287 if label is None: 

288 label = self._labels[path] = self._label(path) 

289 return label 

290 

291 def _label(self, path: Path) -> str: 

292 path = path.resolve() 

293 for top in (*self.dependency_dirs, self.project_dir): 293 ↛ 297line 293 didn't jump to line 297 because the loop on line 293 didn't complete

294 if top is not None and path.is_relative_to(top): 

295 relative = path.relative_to(top).as_posix() 

296 return relative if top == self.project_dir else f"{top.name}/{relative}" 

297 return str(path) 

298 

299 def _fill(self): 

300 files = self.shown_files() 

301 if not self.files: 

302 if self.project_dir is not None: 

303 self.show_message("No Fortran files in this project.") 

304 return 

305 self.empty.hide() 

306 self.body.show() 

307 self.filter_edit.setEnabled(True) 

308 self._update_totals(files) 

309 self._fill_files(files) 

310 self._fill_procedures() 

311 

312 def _update_totals(self, files: list[cx.FileMetrics]): 

313 self.subtitle.setText(" · ".join(part for part in (self.description, f"{len(files)} files") if part)) 

314 procedures = [p for f in files for p in f.procedures] 

315 over = [p for p in procedures if p.warnings(self.thresholds)] 

316 nloc = sum(f.nloc for f in files) 

317 average_ccn = sum(p.ccn for p in procedures) / len(procedures) if procedures else 0.0 

318 average_nloc = sum(p.nloc for p in procedures) / len(procedures) if procedures else 0.0 

319 self.totals.setText(f"{nloc:,} lines of code · {len(procedures):,} procedures · average CCN " 

320 f"{average_ccn:.1f}, NLOC {average_nloc:.0f} · " 

321 + (f"<span style='color:{OVER}'>{len(over)} over a threshold</span>" if over 

322 else "none over a threshold")) 

323 self.totals.setToolTip("Thresholds (lizard's): " + ", ".join( 

324 f"{key.upper() if key == 'ccn' else key} {value}" for key, value in self.thresholds.items())) 

325 

326 def chosen_file(self) -> str | None: 

327 """The file chosen in the files' table (its label), if the filter is 

328 still that file's.""" 

329 row = self.file_table.currentRow() 

330 item = self.file_table.item(row, 0) if row >= 0 else None 

331 label = item.data(Qt.UserRole) if item is not None else None 

332 return label if label and label == self.filter_edit.text() else None 

333 

334 def _listed(self, files: list[cx.FileMetrics]) -> list[tuple[cx.FileMetrics, cx.Procedure, str]]: 

335 """(file, procedure, file's label) of the files' procedures the filter 

336 lets through (a file chosen: only that file's).""" 

337 needle = self.filter_edit.text().strip().lower() 

338 chosen = self.chosen_file() 

339 rows = [] 

340 for f in files: 

341 label = self.label(f.path) 

342 if chosen is not None and label != chosen: # (only that file's: not another path it's a part of) 

343 continue 

344 for proc in f.procedures: 

345 if chosen is None and needle and needle not in proc.qualified.lower() and needle not in label.lower(): 

346 continue 

347 rows.append((f, proc, label)) 

348 return rows 

349 

350 def _fill_procedures(self): 

351 rows = [ProcedureRow(f, proc, label, frozenset(proc.warnings(self.thresholds))) 

352 for f, proc, label in self._listed(self.shown_files())] 

353 self.procedures.setColumnHidden(FILE_COLUMN, self.chosen_file() is not None) # (one file: no need to say which) 

354 table = self.procedures 

355 table.blockSignals(True) # (refilling isn't choosing a procedure: see the end) 

356 table.set_rows(rows) 

357 self._fit_columns(table) 

358 table.blockSignals(False) 

359 self._keep_flow() 

360 

361 def _procedure_color(self, row: ProcedureRow, column: int) -> QColor | None: 

362 if column == FILE_COLUMN: 

363 return QColor(self.palette_colors["muted"]) 

364 return QColor(OVER) if METRIC_OF_COLUMN.get(PROCEDURE_COLUMNS[column]) in row.over else None 

365 

366 def _procedure_bold(self, row: ProcedureRow, column: int) -> bool: 

367 return METRIC_OF_COLUMN.get(PROCEDURE_COLUMNS[column]) in row.over 

368 

369 @staticmethod 

370 def _procedure_data(row: ProcedureRow, role: int): 

371 if role == Qt.UserRole: 

372 return str(row.proc.path), row.proc.start 

373 if role == PROCEDURE_ROLE: 373 ↛ 375line 373 didn't jump to line 375 because the condition on line 373 was always true

374 return row.proc 

375 if role == FILE_ROLE: 

376 return row.file.path 

377 return None 

378 

379 # ------------------------------------------------------------ control flow 

380 def set_code_font(self, font: QFont): 

381 """The graph's font (the editor's).""" 

382 self.flow.set_code_font(font) 

383 

384 def _row_of(self, path: Path, qualified: str) -> int | None: 

385 return next((n for n, row in enumerate(self.procedures.rows()) 

386 if row.proc.path == path and row.proc.qualified == qualified), None) 

387 

388 def _keep_flow(self): 

389 """After the tables are filled again (a save, a filter, a file chosen): 

390 the procedure shown stays selected, its graph drawn again from its file 

391 (it may have changed); filtered out, or a file just chosen, the first 

392 procedure listed is selected and shown instead; gone from the project, 

393 the graph goes too.""" 

394 shown = self.flow_shown 

395 row = self._row_of(shown.path, shown.qualified) if shown is not None else None 

396 if row is None and self.procedures.rowCount() and (shown is not None or self.chosen_file()): 

397 row = 0 # (filtered out, or a file chosen: its most complex procedure) 

398 if row is None and shown is None: 

399 return 

400 if row is None: 400 ↛ 401line 400 didn't jump to line 401 because the condition on line 400 was never true

401 if not any(p.path == shown.path and p.qualified == shown.qualified 

402 for f in self.files for p in f.procedures): 

403 self.flow_shown = None 

404 self.flow_title.setText("") 

405 self.flow.show_message("Select a procedure to see its control flow.") 

406 return 

407 self.procedures.blockSignals(True) 

408 self.procedures.setCurrentCell(row, NAME_COLUMN) 

409 self.procedures.blockSignals(False) 

410 self._show_flow(row, again=True) 

411 

412 def _show_flow(self, row: int, again: bool = False): 

413 """The control-flow graph of the procedure in ``row``.""" 

414 item = self.procedures.item(row, NAME_COLUMN) if row >= 0 else None 

415 proc = item.data(PROCEDURE_ROLE) if item is not None else None 

416 if proc is None or (not again and proc is self.flow_shown): 416 ↛ 417line 416 didn't jump to line 417 because the condition on line 416 was never true

417 return 

418 self.flow_shown = proc 

419 try: 

420 text = read_source(proc.path).text 

421 except OSError as error: 

422 self.flow.show_message(f"Couldn't read {proc.path.name}: {error}") 

423 return 

424 graph = cf.build(text, proc.path, proc) 

425 decisions = graph.complexity - 1 

426 self.flow_title.setText(f"<b>{html.escape(proc.name)}</b> · lines {proc.start}–{proc.end} · " 

427 f"{decisions} decision{'s' * (decisions != 1)} · click a node to open it, " 

428 "right-click for more") 

429 self.flow.show_graph(graph, text) 

430 

431 def _flow_line_clicked(self, path: str, line: int): 

432 if self.flow_shown is not None: 432 ↛ exitline 432 didn't return from function '_flow_line_clicked' because the condition on line 432 was always true

433 self.open_requested.emit(path or str(self.flow_shown.path), line) 

434 

435 def _procedure_tip(self, proc: cx.Procedure, label: str, over: set[str]) -> str: 

436 lines = [f"<b>{html.escape(proc.qualified)}</b> ({proc.kind})", 

437 f"{html.escape(label)}, lines {proc.start}–{proc.end}"] 

438 if over: 

439 names = {"ccn": "CCN", "length": "length", "params": "parameters", "depth": "depth"} 

440 over_text = ", ".join(f"{names[key]} (over {self.thresholds[key]})" for key in sorted(over)) 

441 lines.append(f"<span style='color:{OVER}'>Over: {over_text}</span>") 

442 return "<br>".join(lines) 

443 

444 def _fill_files(self, files: list[cx.FileMetrics]): 

445 table = self.file_table 

446 chosen = self.chosen_file() 

447 table.blockSignals(True) # (refilling isn't choosing a file) 

448 table.set_rows([FileRow(f, self.label(f.path), sum(1 for p in f.procedures if p.warnings(self.thresholds)), 

449 max((p.ccn for p in f.procedures), default=0)) for f in files]) 

450 self._fit_columns(table) 

451 table.setCurrentCell(-1, -1) 

452 row = next((n for n, r in enumerate(table.rows()) if r.label == chosen), None) 

453 if row is not None: # (still chosen: a save refills the tables) 

454 table.setCurrentCell(row, 0) 

455 table.blockSignals(False) 

456 

457 def _procedure_clicked(self, row: int, _column: int): 

458 item = self.procedures.item(row, NAME_COLUMN) 

459 if item is not None: 459 ↛ exitline 459 didn't return from function '_procedure_clicked' because the condition on line 459 was always true

460 path, line = item.data(Qt.UserRole) 

461 self.open_requested.emit(path, line) 

462 

463 def _file_chosen(self, row: int, _column: int, previous_row: int, _previous_column: int): 

464 """The current file changed (a click, or an arrow key): only its procedures.""" 

465 item = self.file_table.item(row, 0) if row >= 0 else None 

466 if item is None or row == previous_row: 466 ↛ 467line 466 didn't jump to line 467 because the condition on line 466 was never true

467 return 

468 # (The click that chose it doesn't also unchoose it; an arrow key isn't a click.) 

469 self._file_just_chosen = bool(QApplication.mouseButtons() & Qt.LeftButton) 

470 self.filter_edit.setText(item.data(Qt.UserRole)) 

471 

472 def _file_clicked(self, row: int, _column: int): 

473 """A click on the file already chosen: all the procedures again.""" 

474 if self._file_just_chosen: 

475 self._file_just_chosen = False 

476 return 

477 item = self.file_table.item(row, 0) 

478 if item is not None and self.filter_edit.text() == item.data(Qt.UserRole): 478 ↛ exitline 478 didn't return from function '_file_clicked' because the condition on line 478 was always true

479 self.filter_edit.clear() 

480 self.file_table.blockSignals(True) 

481 self.file_table.setCurrentCell(-1, -1) 

482 self.file_table.blockSignals(False)