vibefpmgui.problems_panel
The Problems tab: everything flagged in the file in the editor.
The last build's compiler messages, fortitude's and fortls's issues,
misspelled words, gremlins (non-ASCII and invisible characters) and the AI
review's findings, in line order, drawn like
the Search tab's results (vibefpmgui.result_tree). A button per tool
(with its count) shows or hides its problems. Click a problem to go there.
1"""The *Problems* tab: everything flagged in the file in the editor. 2 3The last build's compiler messages, fortitude's and fortls's issues, 4misspelled words, gremlins (non-ASCII and invisible characters) and the AI 5review's findings, in line order, drawn like 6the Search tab's results (`vibefpmgui.result_tree`). A button per tool 7(with its count) shows or hides its problems. Click a problem to go there. 8""" 9 10from __future__ import annotations 11 12from pathlib import Path 13 14from qtpy.QtCore import Qt, Signal 15from qtpy.QtWidgets import ( 16 QApplication, 17 QHBoxLayout, 18 QMenu, 19 QToolButton, 20 QVBoxLayout, 21 QWidget, 22) 23 24from .result_tree import RESULT_ROLE, Problem, ResultTree 25from .widgets import StatusLabel 26 27# (source, button label), in the order the buttons are shown. 28SOURCES = [("build", "Build"), ("fortitude", "fortitude"), ("fortls", "fortls"), 29 ("spelling", "Spelling"), ("gremlins", "Gremlins"), ("review", "AI review")] 30HINT = "Open a file in the editor to see its problems here." 31EXPLAINABLE = ("build", "fortitude", "fortls") # (sources Explain with AI is offered for) 32 33 34class ProblemsPanel(QWidget): 35 open_requested = Signal(str, int, int, int, int) # file, line, column, end line, end column 36 explain_requested = Signal(object) # a Problem (right-click → Explain with AI) 37 fix_requested = Signal(object, object) # its file's path and a Problem (right-click → Fix with AI…) 38 apply_fix_requested = Signal(object, object) # its file's path and a Problem (right-click → Apply Fix) 39 40 def __init__(self, settings=None, parent=None): 41 super().__init__(parent) 42 self.settings = settings 43 self.path: Path | None = None 44 self.problems: list[Problem] = [] 45 self.labels: tuple[str, str] = ("", "") # (folder, package) shown after the file's name 46 self.title = StatusLabel(HINT) 47 self.title.setContentsMargins(8, 4, 8, 2) 48 self.buttons: dict[str, QToolButton] = {} 49 header = QHBoxLayout() 50 header.setContentsMargins(0, 0, 6, 0) 51 header.setSpacing(4) 52 header.addWidget(self.title, 1) 53 for source, label in SOURCES: 54 button = QToolButton() 55 button.setCheckable(True) 56 button.setAutoRaise(True) 57 shown = settings.value(f"problems/{source}", True, type=bool) if settings is not None else True 58 button.setChecked(shown) 59 button.setToolTip(f"Show or hide {label}'s problems") 60 button.toggled.connect(self._filter_changed) 61 button.setProperty("label", label) 62 self.buttons[source] = button 63 header.addWidget(button) 64 self.tree = ResultTree() 65 self.tree.set_wrapping(True) # (long messages wrap: an AI review's can be a few sentences) 66 self.tree.open_requested.connect(self.open_requested) 67 self.tree.setContextMenuPolicy(Qt.CustomContextMenu) 68 self.tree.customContextMenuRequested.connect(self._context_menu) 69 layout = QVBoxLayout(self) 70 layout.setContentsMargins(0, 0, 0, 0) 71 layout.setSpacing(2) 72 layout.addLayout(header) 73 layout.addWidget(self.tree, 1) 74 self.show_problems(None, []) 75 76 def _context_menu(self, pos): 77 menu = self.context_menu(pos) 78 if menu is not None: 79 menu.exec(self.tree.viewport().mapToGlobal(pos)) 80 menu.deleteLater() 81 82 def context_menu(self, pos) -> QMenu | None: 83 """A problem's right-click menu (None: not on a problem).""" 84 item = self.tree.itemAt(pos) 85 data = item.data(0, RESULT_ROLE) if item is not None else None 86 path, problem = data if data else (None, None) 87 if not isinstance(problem, Problem): 88 return None 89 menu = QMenu(self) 90 issue = problem.issue 91 if issue is not None and issue.fixable: # (fortitude's own fix, as its hover card's Apply fix) 92 label = f"Apply Fix: {issue.fix or issue.message}" + ("" if issue.fix_safe else " (unsafe — check it)") 93 apply = menu.addAction(label, lambda: self.apply_fix_requested.emit(path, problem)) 94 apply.setToolTip("Change the code as fortitude suggests (one Undo step)") 95 menu.addSeparator() 96 explain = menu.addAction("Explain with AI", lambda: self.explain_requested.emit(problem)) 97 explain.setEnabled(problem.source in EXPLAINABLE) 98 explain.setToolTip("Ask the local AI model what this means here, and how to fix it") 99 fix = menu.addAction("Fix with AI…", lambda: self.fix_requested.emit(path, problem)) 100 fix.setEnabled(problem.source in (*EXPLAINABLE, "review")) 101 fix.setToolTip("Ask the local AI model for an edit that addresses this (you accept or reject " 102 "each change)") 103 menu.addAction("Copy Message", lambda: QApplication.clipboard().setText(problem.message)) 104 menu.setToolTipsVisible(True) 105 return menu 106 107 def _filter_changed(self): 108 if self.settings is not None: 109 for source, button in self.buttons.items(): 110 self.settings.setValue(f"problems/{source}", button.isChecked()) 111 self._refresh() 112 113 def show_problems(self, path: Path | None, problems: list[Problem], folder: str = "", package: str = ""): 114 """The file in the editor and its problems (``path`` None: no file).""" 115 self.path, self.problems, self.labels = path, list(problems), (folder, package) 116 self._refresh() 117 118 def shown(self) -> list[Problem]: 119 """The problems whose tool's button is on.""" 120 return [p for p in self.problems if self.buttons[p.source].isChecked()] 121 122 def _refresh(self): 123 counts = {source: 0 for source in self.buttons} 124 for p in self.problems: 125 counts[p.source] += 1 126 for source, button in self.buttons.items(): 127 button.setText(f"{button.property('label')} {counts[source]}") 128 button.setVisible(counts[source] > 0 or not button.isChecked()) # (a hidden kind stays findable) 129 if self.path is None: 130 self.tree.clear_results() 131 self.title.setText(HINT) 132 return 133 shown = self.shown() 134 self.tree.set_problems(self.path, shown, *self.labels) 135 total = len(self.problems) 136 if not total: 137 self.title.setText(f"No problems in <b>{self.path.name}</b>") 138 else: 139 hidden = total - len(shown) 140 self.title.setText(f"<b>{total}</b> problem{'s' * (total != 1)}" 141 + (f" ({hidden} hidden)" if hidden else ""))
35class ProblemsPanel(QWidget): 36 open_requested = Signal(str, int, int, int, int) # file, line, column, end line, end column 37 explain_requested = Signal(object) # a Problem (right-click → Explain with AI) 38 fix_requested = Signal(object, object) # its file's path and a Problem (right-click → Fix with AI…) 39 apply_fix_requested = Signal(object, object) # its file's path and a Problem (right-click → Apply Fix) 40 41 def __init__(self, settings=None, parent=None): 42 super().__init__(parent) 43 self.settings = settings 44 self.path: Path | None = None 45 self.problems: list[Problem] = [] 46 self.labels: tuple[str, str] = ("", "") # (folder, package) shown after the file's name 47 self.title = StatusLabel(HINT) 48 self.title.setContentsMargins(8, 4, 8, 2) 49 self.buttons: dict[str, QToolButton] = {} 50 header = QHBoxLayout() 51 header.setContentsMargins(0, 0, 6, 0) 52 header.setSpacing(4) 53 header.addWidget(self.title, 1) 54 for source, label in SOURCES: 55 button = QToolButton() 56 button.setCheckable(True) 57 button.setAutoRaise(True) 58 shown = settings.value(f"problems/{source}", True, type=bool) if settings is not None else True 59 button.setChecked(shown) 60 button.setToolTip(f"Show or hide {label}'s problems") 61 button.toggled.connect(self._filter_changed) 62 button.setProperty("label", label) 63 self.buttons[source] = button 64 header.addWidget(button) 65 self.tree = ResultTree() 66 self.tree.set_wrapping(True) # (long messages wrap: an AI review's can be a few sentences) 67 self.tree.open_requested.connect(self.open_requested) 68 self.tree.setContextMenuPolicy(Qt.CustomContextMenu) 69 self.tree.customContextMenuRequested.connect(self._context_menu) 70 layout = QVBoxLayout(self) 71 layout.setContentsMargins(0, 0, 0, 0) 72 layout.setSpacing(2) 73 layout.addLayout(header) 74 layout.addWidget(self.tree, 1) 75 self.show_problems(None, []) 76 77 def _context_menu(self, pos): 78 menu = self.context_menu(pos) 79 if menu is not None: 80 menu.exec(self.tree.viewport().mapToGlobal(pos)) 81 menu.deleteLater() 82 83 def context_menu(self, pos) -> QMenu | None: 84 """A problem's right-click menu (None: not on a problem).""" 85 item = self.tree.itemAt(pos) 86 data = item.data(0, RESULT_ROLE) if item is not None else None 87 path, problem = data if data else (None, None) 88 if not isinstance(problem, Problem): 89 return None 90 menu = QMenu(self) 91 issue = problem.issue 92 if issue is not None and issue.fixable: # (fortitude's own fix, as its hover card's Apply fix) 93 label = f"Apply Fix: {issue.fix or issue.message}" + ("" if issue.fix_safe else " (unsafe — check it)") 94 apply = menu.addAction(label, lambda: self.apply_fix_requested.emit(path, problem)) 95 apply.setToolTip("Change the code as fortitude suggests (one Undo step)") 96 menu.addSeparator() 97 explain = menu.addAction("Explain with AI", lambda: self.explain_requested.emit(problem)) 98 explain.setEnabled(problem.source in EXPLAINABLE) 99 explain.setToolTip("Ask the local AI model what this means here, and how to fix it") 100 fix = menu.addAction("Fix with AI…", lambda: self.fix_requested.emit(path, problem)) 101 fix.setEnabled(problem.source in (*EXPLAINABLE, "review")) 102 fix.setToolTip("Ask the local AI model for an edit that addresses this (you accept or reject " 103 "each change)") 104 menu.addAction("Copy Message", lambda: QApplication.clipboard().setText(problem.message)) 105 menu.setToolTipsVisible(True) 106 return menu 107 108 def _filter_changed(self): 109 if self.settings is not None: 110 for source, button in self.buttons.items(): 111 self.settings.setValue(f"problems/{source}", button.isChecked()) 112 self._refresh() 113 114 def show_problems(self, path: Path | None, problems: list[Problem], folder: str = "", package: str = ""): 115 """The file in the editor and its problems (``path`` None: no file).""" 116 self.path, self.problems, self.labels = path, list(problems), (folder, package) 117 self._refresh() 118 119 def shown(self) -> list[Problem]: 120 """The problems whose tool's button is on.""" 121 return [p for p in self.problems if self.buttons[p.source].isChecked()] 122 123 def _refresh(self): 124 counts = {source: 0 for source in self.buttons} 125 for p in self.problems: 126 counts[p.source] += 1 127 for source, button in self.buttons.items(): 128 button.setText(f"{button.property('label')} {counts[source]}") 129 button.setVisible(counts[source] > 0 or not button.isChecked()) # (a hidden kind stays findable) 130 if self.path is None: 131 self.tree.clear_results() 132 self.title.setText(HINT) 133 return 134 shown = self.shown() 135 self.tree.set_problems(self.path, shown, *self.labels) 136 total = len(self.problems) 137 if not total: 138 self.title.setText(f"No problems in <b>{self.path.name}</b>") 139 else: 140 hidden = total - len(shown) 141 self.title.setText(f"<b>{total}</b> problem{'s' * (total != 1)}" 142 + (f" ({hidden} hidden)" if hidden else ""))
QWidget(self, /, parent: PySide6.QtWidgets.QWidget | None = None, f: PySide6.QtCore.Qt.WindowType = Default(Qt.WindowFlags), *, modal: bool | None = None, windowModality: PySide6.QtCore.Qt.WindowModality | None = None, enabled: bool | None = None, geometry: PySide6.QtCore.QRect | None = None, frameGeometry: PySide6.QtCore.QRect | None = None, normalGeometry: PySide6.QtCore.QRect | None = None, x: int | None = None, y: int | None = None, pos: PySide6.QtCore.QPoint | None = None, frameSize: PySide6.QtCore.QSize | None = None, size: PySide6.QtCore.QSize | None = None, width: int | None = None, height: int | None = None, rect: PySide6.QtCore.QRect | None = None, childrenRect: PySide6.QtCore.QRect | None = None, childrenRegion: PySide6.QtGui.QRegion | None = None, sizePolicy: PySide6.QtWidgets.QSizePolicy | None = None, minimumSize: PySide6.QtCore.QSize | None = None, maximumSize: PySide6.QtCore.QSize | None = None, minimumWidth: int | None = None, minimumHeight: int | None = None, maximumWidth: int | None = None, maximumHeight: int | None = None, sizeIncrement: PySide6.QtCore.QSize | None = None, baseSize: PySide6.QtCore.QSize | None = None, palette: PySide6.QtGui.QPalette | None = None, font: PySide6.QtGui.QFont | None = None, cursor: PySide6.QtGui.QCursor | None = None, mouseTracking: bool | None = None, tabletTracking: bool | None = None, isActiveWindow: bool | None = None, focusPolicy: PySide6.QtCore.Qt.FocusPolicy | None = None, focus: bool | None = None, contextMenuPolicy: PySide6.QtCore.Qt.ContextMenuPolicy | None = None, updatesEnabled: bool | None = None, visible: bool | None = None, minimized: bool | None = None, maximized: bool | None = None, fullScreen: bool | None = None, sizeHint: PySide6.QtCore.QSize | None = None, minimumSizeHint: PySide6.QtCore.QSize | None = None, acceptDrops: bool | None = None, windowTitle: str | None = None, windowIcon: PySide6.QtGui.QIcon | None = None, windowIconText: str | None = None, windowOpacity: float | None = None, windowModified: bool | None = None, toolTip: str | None = None, toolTipDuration: int | None = None, statusTip: str | None = None, whatsThis: str | None = None, accessibleName: str | None = None, accessibleDescription: str | None = None, accessibleIdentifier: str | None = None, layoutDirection: PySide6.QtCore.Qt.LayoutDirection | None = None, autoFillBackground: bool | None = None, styleSheet: str | None = None, locale: PySide6.QtCore.QLocale | None = None, windowFilePath: str | None = None, inputMethodHints: PySide6.QtCore.Qt.InputMethodHint | None = None) -> None
41 def __init__(self, settings=None, parent=None): 42 super().__init__(parent) 43 self.settings = settings 44 self.path: Path | None = None 45 self.problems: list[Problem] = [] 46 self.labels: tuple[str, str] = ("", "") # (folder, package) shown after the file's name 47 self.title = StatusLabel(HINT) 48 self.title.setContentsMargins(8, 4, 8, 2) 49 self.buttons: dict[str, QToolButton] = {} 50 header = QHBoxLayout() 51 header.setContentsMargins(0, 0, 6, 0) 52 header.setSpacing(4) 53 header.addWidget(self.title, 1) 54 for source, label in SOURCES: 55 button = QToolButton() 56 button.setCheckable(True) 57 button.setAutoRaise(True) 58 shown = settings.value(f"problems/{source}", True, type=bool) if settings is not None else True 59 button.setChecked(shown) 60 button.setToolTip(f"Show or hide {label}'s problems") 61 button.toggled.connect(self._filter_changed) 62 button.setProperty("label", label) 63 self.buttons[source] = button 64 header.addWidget(button) 65 self.tree = ResultTree() 66 self.tree.set_wrapping(True) # (long messages wrap: an AI review's can be a few sentences) 67 self.tree.open_requested.connect(self.open_requested) 68 self.tree.setContextMenuPolicy(Qt.CustomContextMenu) 69 self.tree.customContextMenuRequested.connect(self._context_menu) 70 layout = QVBoxLayout(self) 71 layout.setContentsMargins(0, 0, 0, 0) 72 layout.setSpacing(2) 73 layout.addLayout(header) 74 layout.addWidget(self.tree, 1) 75 self.show_problems(None, [])
__init__(self, /, parent: PySide6.QtWidgets.QWidget | None = None, f: PySide6.QtCore.Qt.WindowType = Default(Qt.WindowFlags), *, modal: bool | None = None, windowModality: PySide6.QtCore.Qt.WindowModality | None = None, enabled: bool | None = None, geometry: PySide6.QtCore.QRect | None = None, frameGeometry: PySide6.QtCore.QRect | None = None, normalGeometry: PySide6.QtCore.QRect | None = None, x: int | None = None, y: int | None = None, pos: PySide6.QtCore.QPoint | None = None, frameSize: PySide6.QtCore.QSize | None = None, size: PySide6.QtCore.QSize | None = None, width: int | None = None, height: int | None = None, rect: PySide6.QtCore.QRect | None = None, childrenRect: PySide6.QtCore.QRect | None = None, childrenRegion: PySide6.QtGui.QRegion | None = None, sizePolicy: PySide6.QtWidgets.QSizePolicy | None = None, minimumSize: PySide6.QtCore.QSize | None = None, maximumSize: PySide6.QtCore.QSize | None = None, minimumWidth: int | None = None, minimumHeight: int | None = None, maximumWidth: int | None = None, maximumHeight: int | None = None, sizeIncrement: PySide6.QtCore.QSize | None = None, baseSize: PySide6.QtCore.QSize | None = None, palette: PySide6.QtGui.QPalette | None = None, font: PySide6.QtGui.QFont | None = None, cursor: PySide6.QtGui.QCursor | None = None, mouseTracking: bool | None = None, tabletTracking: bool | None = None, isActiveWindow: bool | None = None, focusPolicy: PySide6.QtCore.Qt.FocusPolicy | None = None, focus: bool | None = None, contextMenuPolicy: PySide6.QtCore.Qt.ContextMenuPolicy | None = None, updatesEnabled: bool | None = None, visible: bool | None = None, minimized: bool | None = None, maximized: bool | None = None, fullScreen: bool | None = None, sizeHint: PySide6.QtCore.QSize | None = None, minimumSizeHint: PySide6.QtCore.QSize | None = None, acceptDrops: bool | None = None, windowTitle: str | None = None, windowIcon: PySide6.QtGui.QIcon | None = None, windowIconText: str | None = None, windowOpacity: float | None = None, windowModified: bool | None = None, toolTip: str | None = None, toolTipDuration: int | None = None, statusTip: str | None = None, whatsThis: str | None = None, accessibleName: str | None = None, accessibleDescription: str | None = None, accessibleIdentifier: str | None = None, layoutDirection: PySide6.QtCore.Qt.LayoutDirection | None = None, autoFillBackground: bool | None = None, styleSheet: str | None = None, locale: PySide6.QtCore.QLocale | None = None, windowFilePath: str | None = None, inputMethodHints: PySide6.QtCore.Qt.InputMethodHint | None = None) -> None
Initialize self. See help(type(self)) for accurate signature.
114 def show_problems(self, path: Path | None, problems: list[Problem], folder: str = "", package: str = ""): 115 """The file in the editor and its problems (``path`` None: no file).""" 116 self.path, self.problems, self.labels = path, list(problems), (folder, package) 117 self._refresh()
The file in the editor and its problems (path None: no file).
119 def shown(self) -> list[Problem]: 120 """The problems whose tool's button is on.""" 121 return [p for p in self.problems if self.buttons[p.source].isChecked()]
The problems whose tool's button is on.