Coverage for vibefpmgui/window/view.py: 95%

479 statements  

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

1"""The View menu: text size, fonts, theme, colors and the editor's options. 

2 

3Each option is saved in the settings and applied to every open editor. 

4""" 

5 

6from __future__ import annotations 

7 

8import sys 

9 

10from qtpy.QtCore import QEvent, QSize, Qt, QUrl 

11from qtpy.QtGui import ( 

12 QAction, 

13 QActionGroup, 

14 QDesktopServices, 

15 QFont, 

16 QFontDatabase, 

17 QKeySequence, 

18) 

19from qtpy.QtWidgets import QApplication, QFontDialog, QMessageBox, QPlainTextEdit 

20 

21from .. import editor as editor_mod 

22from .. import highlighter as hl 

23from .. import lint, lsp, spelling, themes 

24from ..about import FPM_DOCS_URL, AboutDialog 

25from ..build_monitor import set_ui_scale 

26from ..code_editor import CodeEditor 

27from ..command_palette import Command, CommandPalette 

28from ..editor import EditorTab, monospace_font 

29from ..editor_shared import INDENT_WIDTHS 

30from ..widgets import run_dialog 

31 

32 

33class ViewMixin: 

34 """The View menu and how the window looks (a part of `vibefpmgui.main_window.MainWindow`).""" 

35 

36 def _build_view_menu(self): 

37 """View menu: text size, editor options, theme and colors (Panels: _build_docks).""" 

38 # View menu: text size for the whole window. 

39 # The system font, recorded once: later windows would otherwise see the 

40 # already-enlarged application font as "normal". 

41 app = QApplication.instance() 

42 if app.property("vibefpm_base_font") is None: 

43 app.setProperty("vibefpm_base_font", QFont(app.font())) 

44 self._base_font = QFont(app.property("vibefpm_base_font")) 

45 self.font_delta = 0 

46 view_menu = self.view_menu = self.menuBar().addMenu("&View") 

47 # Every command in the menus, found by typing (see vibefpmgui.command_palette). 

48 self.palette_action = view_menu.addAction("Command Palette…") 

49 self.palette_action.setShortcut(QKeySequence("Ctrl+Shift+P")) # (⌘⇧P on macOS) 

50 self.palette_action.setStatusTip("Find any command in the menus by typing part of its name") 

51 self.palette_action.triggered.connect(self.show_command_palette) 

52 self._command_palette: CommandPalette | None = None 

53 view_menu.addSeparator() 

54 self.bigger_action = QAction("Bigger Text", self) 

55 self.bigger_action.setShortcuts([QKeySequence(QKeySequence.ZoomIn), QKeySequence("Ctrl+=")]) 

56 self.bigger_action.triggered.connect(lambda: self.set_font_delta(self.font_delta + 1)) 

57 self.smaller_action = QAction("Smaller Text", self) 

58 self.smaller_action.setShortcut(QKeySequence(QKeySequence.ZoomOut)) 

59 self.smaller_action.triggered.connect(lambda: self.set_font_delta(self.font_delta - 1)) 

60 self.reset_font_action = QAction("Actual Size", self) # (Ctrl+0 is Graph → Fit to window) 

61 self.reset_font_action.triggered.connect(lambda: self.set_font_delta(0)) 

62 view_menu.addActions([self.bigger_action, self.smaller_action, self.reset_font_action]) 

63 editor_mod.editor_font_family = self.settings.value("editor_font_family", "") or "" 

64 editor_mod.editor_font_size = self.settings.value("editor_font_size", 0.0, type=float) or 0.0 

65 self.editor_font_action = view_menu.addAction("Editor Font…") 

66 self.editor_font_action.setStatusTip("Choose the font for the editor and the fpm.toml tab") 

67 self.editor_font_action.triggered.connect(self.choose_editor_font) 

68 self.reset_editor_font_action = view_menu.addAction("Reset Editor Font") 

69 self.reset_editor_font_action.setStatusTip("Use the system's fixed-width font in the editor again") 

70 self.reset_editor_font_action.triggered.connect(lambda: self.set_editor_font("", 0.0)) 

71 self.reset_editor_font_action.setEnabled(bool(editor_mod.editor_font_family 

72 or editor_mod.editor_font_size)) 

73 view_menu.addSeparator() 

74 self.use_editor_action = QAction("Open Files in Built-in Editor", self, checkable=True) 

75 self.use_editor_action.setChecked(self.settings.value("use_editor", True, type=bool)) 

76 self.use_editor_action.toggled.connect(lambda on: self.settings.setValue("use_editor", on)) 

77 self.use_editor_action.setStatusTip( 

78 "Double-clicked files open in a tab here; turn off to use your default application") 

79 view_menu.addAction(self.use_editor_action) 

80 self.lint_action = QAction("Lint Fortran Files (fortitude)", self, checkable=True) 

81 EditorTab.lint_enabled = self.settings.value("lint", True, type=bool) 

82 self.lint_action.setChecked(EditorTab.lint_enabled) 

83 self.lint_action.toggled.connect(self.set_lint_enabled) 

84 if lint.executable() is None: 84 ↛ 85line 84 didn't jump to line 85 because the condition on line 84 was never true

85 self.lint_action.setEnabled(False) 

86 self.lint_action.setText("Lint Fortran Files (fortitude not found)") 

87 self.lint_action.setStatusTip("Check free-form Fortran files in the editor with fortitude " 

88 "as you type; findings are underlined and marked in the gutter") 

89 view_menu.addAction(self.lint_action) 

90 self.lsp_diagnostics_action = QAction("Language Server Diagnostics (fortls)", self, checkable=True) 

91 EditorTab.lsp_diagnostics_enabled = self.settings.value("lsp_diagnostics", True, type=bool) 

92 self.lsp_diagnostics_action.setChecked(EditorTab.lsp_diagnostics_enabled) 

93 self.lsp_diagnostics_action.toggled.connect(self.set_lsp_diagnostics_enabled) 

94 self.lsp_diagnostics_action.setStatusTip( 

95 "Show the problems fortls finds in a Fortran file when it's opened or saved (e.g. USE after " 

96 "IMPLICIT, misplaced CONTAINS, interface mismatches), marked like fortitude's") 

97 view_menu.addAction(self.lsp_diagnostics_action) 

98 self.spelling_action = QAction("Check Spelling in Comments and Strings", self, checkable=True) 

99 EditorTab.spelling_enabled = self.settings.value("spelling", True, type=bool) 

100 spelling.user_words = {w for w in str(self.settings.value("spelling_words", "") or "").split()} 

101 self.spelling_action.setChecked(EditorTab.spelling_enabled) 

102 self.spelling_action.toggled.connect(self.set_spelling_enabled) 

103 if not spelling.available(): 103 ↛ 104line 103 didn't jump to line 104 because the condition on line 103 was never true

104 self.spelling_action.setEnabled(False) 

105 self.spelling_action.setText("Check Spelling (pyspellchecker not found)") 

106 self.spelling_action.setStatusTip( 

107 "Underline misspelled words in comments and strings (right-click one for suggestions)") 

108 view_menu.addSeparator() # (spelling: its two items together) 

109 view_menu.addAction(self.spelling_action) 

110 spelling.set_language(str(self.settings.value("spelling_language", "en") or "en")) 

111 self.spelling_language_menu = view_menu.addMenu("Spelling Language") # (kept: PySide would let it go) 

112 group = QActionGroup(self.spelling_language_menu) 

113 codes = sorted(spelling.languages(), key=lambda c: spelling.LANGUAGE_NAMES.get(c, c)) 

114 for code in codes: 

115 action = self.spelling_language_menu.addAction(f"{spelling.LANGUAGE_NAMES.get(code, code)} ({code})") 

116 action.setCheckable(True) 

117 action.setChecked(code == spelling.language) 

118 action.setData(code) 

119 group.addAction(action) 

120 action.triggered.connect(lambda _=False, c=code: self.set_spelling_language(c)) 

121 self.spelling_language_menu.setEnabled(bool(codes)) 

122 view_menu.addSeparator() 

123 self.gremlins_action = QAction("Show Gremlins (Non-ASCII and Invisible Characters)", self, checkable=True) 

124 EditorTab.gremlins_enabled = self.settings.value("gremlins", True, type=bool) 

125 self.gremlins_action.setChecked(EditorTab.gremlins_enabled) 

126 self.gremlins_action.toggled.connect(self.set_gremlins_enabled) 

127 self.gremlins_action.setStatusTip( 

128 "Mark lines with characters that aren't plain ASCII (accented letters, smart quotes, " 

129 "non-breaking or zero-width spaces, control characters) with a gremlin in the gutter") 

130 view_menu.addAction(self.gremlins_action) 

131 self.minimap_action = QAction("Show Minimap", self, checkable=True) 

132 EditorTab.minimap_enabled = self.settings.value("minimap", True, type=bool) 

133 self.minimap_action.setChecked(EditorTab.minimap_enabled) 

134 self.minimap_action.setStatusTip("A zoomed-out view of the whole file at the right of the editor") 

135 self.minimap_action.toggled.connect(self.set_minimap_enabled) 

136 view_menu.addAction(self.minimap_action) 

137 self.blame_action = QAction("Show Git Blame", self, checkable=True) 

138 EditorTab.blame_enabled = self.settings.value("git_blame", False, type=bool) 

139 self.blame_action.setChecked(EditorTab.blame_enabled) 

140 self.blame_action.setStatusTip("Beside the editor, each line's last commit (hash, author, date); " 

141 "click one to open it on GitHub or GitLab") 

142 self.blame_action.toggled.connect(self.set_blame_enabled) 

143 view_menu.addAction(self.blame_action) 

144 self.coverage_action = QAction("Show Coverage in the Editor", self, checkable=True) 

145 CodeEditor.show_coverage = self.settings.value("show_coverage", True, type=bool) 

146 self.coverage_action.setChecked(CodeEditor.show_coverage) 

147 self.coverage_action.setStatusTip("After a test run with Build options → Coverage: green and red bars in the " 

148 "gutter for the lines the tests ran and didn't") 

149 self.coverage_action.toggled.connect(self.set_show_coverage) 

150 view_menu.addAction(self.coverage_action) 

151 self.matches_action = QAction("Highlight Other Occurrences of the Selection", self, checkable=True) 

152 CodeEditor.highlight_matches = self.settings.value("highlight_matches", True, type=bool) 

153 self.matches_action.setChecked(CodeEditor.highlight_matches) 

154 self.matches_action.setStatusTip("Selecting text in the editor highlights it everywhere else in the file " 

155 "(Match case and Whole words as set in the find bar)") 

156 self.matches_action.toggled.connect(self.set_highlight_matches) 

157 view_menu.addAction(self.matches_action) 

158 self.word_wrap_action = QAction("Word Wrap", self, checkable=True) 

159 self.word_wrap_action.setShortcut(QKeySequence("Alt+Z")) 

160 EditorTab.word_wrap = self.settings.value("word_wrap", False, type=bool) 

161 self.word_wrap_action.setChecked(EditorTab.word_wrap) 

162 self.word_wrap_action.setStatusTip("Wrap long lines in the editor, the terminal and the output at the " 

163 "window's edge instead of scrolling sideways") 

164 self.word_wrap_action.toggled.connect(self.set_word_wrap) 

165 view_menu.addAction(self.word_wrap_action) 

166 # Indentation: what Tab types, and how wide tabs are shown. 

167 width = self.settings.value("indent_width", 4, type=int) 

168 CodeEditor.indent_width = width if width in INDENT_WIDTHS else 4 

169 CodeEditor.use_tabs = self.settings.value("indent_tabs", False, type=bool) 

170 self.indent_menu = view_menu.addMenu("Indentation") # (kept: PySide would let it go) 

171 self.indent_actions: dict[object, QAction] = {} 

172 kinds = QActionGroup(self.indent_menu) 

173 for use_tabs, text, tip in ((False, "Indent with Spaces", "Tab types spaces, to the next tab stop"), 

174 (True, "Indent with Tabs", "Tab types a tab character")): 

175 action = self.indent_menu.addAction(text) 

176 action.setCheckable(True) 

177 action.setStatusTip(tip) 

178 action.triggered.connect(lambda _=False, t=use_tabs: self.set_indentation(use_tabs=t)) 

179 kinds.addAction(action) 

180 self.indent_actions[use_tabs] = action 

181 self.indent_menu.addSeparator() 

182 widths = QActionGroup(self.indent_menu) 

183 for w in INDENT_WIDTHS: 

184 action = self.indent_menu.addAction(f"Width: {w}") 

185 action.setCheckable(True) 

186 action.setStatusTip(f"An indent level is {w} columns (tabs are shown {w} wide too)") 

187 action.triggered.connect(lambda _=False, n=w: self.set_indentation(width=n)) 

188 widths.addAction(action) 

189 self.indent_actions[w] = action 

190 self._check_indent_actions() 

191 view_menu.addSeparator() 

192 self.terminal_action = QAction("Terminal", self) 

193 # ⌃` as in VS Code (the Control key on macOS too: ⌘` switches windows). 

194 self.terminal_action.setShortcut(QKeySequence("Meta+`" if sys.platform == "darwin" else "Ctrl+`")) 

195 self.terminal_action.setStatusTip("Your shell, in the project's folder (Terminal tab)") 

196 self.terminal_action.triggered.connect(self.show_terminal) 

197 view_menu.addAction(self.terminal_action) 

198 self.fortls_action = QAction("Fortran Language Server (fortls)", self, checkable=True) 

199 self.fortls_action.setChecked(self.settings.value("fortls", True, type=bool)) 

200 self.fortls_action.toggled.connect(self.set_fortls_enabled) 

201 if lsp.executable() is None: 201 ↛ 202line 201 didn't jump to line 202 because the condition on line 201 was never true

202 self.fortls_action.setEnabled(False) 

203 self.fortls_action.setText("Fortran Language Server (fortls not found)") 

204 self.fortls_action.setStatusTip("Hover information, Go to Definition, the outline and " 

205 "Go to Symbol for Fortran files, from fortls") 

206 view_menu.addAction(self.fortls_action) 

207 view_menu.addSeparator() 

208 self.theme_menu = view_menu.addMenu("Theme") # (kept: PySide would let it go) 

209 self.theme_menu.setStatusTip("The colors of the whole window (Syntax Colors are the editor's)") 

210 self.theme_actions: dict[str, QAction] = {} 

211 group = QActionGroup(self.theme_menu) 

212 previous = None 

213 for key, theme in themes.THEMES.items(): 

214 kind = "native" if theme.native else "dark" if theme.dark else "light" 

215 if previous is not None and kind != previous: 

216 self.theme_menu.addSeparator() # (System/Light/Dark · dark colorful · light colorful) 

217 previous = kind 

218 action = self.theme_menu.addAction(theme.title) 

219 action.setCheckable(True) 

220 action.setChecked(key == themes.current) 

221 action.triggered.connect(lambda _=False, k=key: self.set_theme(k)) 

222 group.addAction(action) 

223 self.theme_actions[key] = action 

224 # The style that draws the window: the theme's, or one chosen. 

225 self.window_style_menu = view_menu.addMenu("Window Style") # (kept: PySide would let it go) 

226 self.window_style_menu.setStatusTip("How buttons, tabs and the panels' dividers are drawn: the " 

227 "theme's choice, or one of Qt's styles") 

228 self.window_style_menu.aboutToShow.connect(self._fill_window_style_menu) 

229 # The toolbar: small icons, labels or not (also its right-click menu: see running._build_commands). 

230 self.toolbar_menu = view_menu.addMenu("Toolbar") # (kept: PySide would let it go) 

231 self.small_icons_action = self.toolbar_menu.addAction("Small Icons") 

232 self.small_icons_action.setCheckable(True) 

233 self.small_icons_action.setChecked(self.settings.value("toolbar_small_icons", False, type=bool)) 

234 self.small_icons_action.setStatusTip("Smaller icons on the toolbar (the window's text size still counts)") 

235 self.toolbar_labels_action = self.toolbar_menu.addAction("Show Labels") 

236 self.toolbar_labels_action.setCheckable(True) 

237 self.toolbar_labels_action.setChecked(self.settings.value("toolbar_labels", True, type=bool)) 

238 self.toolbar_labels_action.setStatusTip("The commands' names under the toolbar's icons (off: icons only, " 

239 "their names in the tooltips)") 

240 self.small_icons_action.toggled.connect(lambda on: self._set_toolbar_option("toolbar_small_icons", on)) 

241 self.toolbar_labels_action.toggled.connect(lambda on: self._set_toolbar_option("toolbar_labels", on)) 

242 hl.chosen_style = self.settings.value("syntax_style", hl.AUTO) or hl.AUTO 

243 self.style_menu = view_menu.addMenu("Syntax Colors") 

244 self.style_menu.setStatusTip("The Pygments color style for the editor and fpm.toml") 

245 self.style_menu.aboutToShow.connect(self._fill_style_menu) 

246 

247 def _apply_highlight_style(self): 

248 """The chosen Pygments style, or one matching the system's light/dark mode.""" 

249 for tree in getattr(self, "result_trees", []): 

250 tree.refresh_style() 

251 if hasattr(self, "debug_panel"): 251 ↛ exitline 251 didn't return from function '_apply_highlight_style' because the condition on line 251 was always true

252 self.debug_panel.breakpoints.refresh_style() 

253 

254 def _fill_style_menu(self): 

255 """View → Syntax Colors: Automatic, then every installed Pygments style.""" 

256 menu = self.style_menu 

257 menu.clear() 

258 group = QActionGroup(menu) 

259 auto = menu.addAction(f"Automatic ({hl.LIGHT_STYLE} / {hl.DARK_STYLE}, following light or dark mode)") 

260 auto.setData(hl.AUTO) 

261 menu.addSeparator() 

262 light, dark = hl.available_styles() 

263 actions = [auto] 

264 for title, names in (("Light Styles", light), ("Dark Styles", dark)): 

265 sub = menu.addMenu(title) 

266 for name in names: 

267 action = sub.addAction(name) 

268 action.setData(name) 

269 actions.append(action) 

270 for action in actions: 

271 action.setCheckable(True) 

272 action.setChecked(action.data() == hl.chosen_style) 

273 group.addAction(action) 

274 action.triggered.connect(lambda _checked=False, a=action: self.set_syntax_style(a.data())) 

275 

276 def show_command_palette(self): 

277 """View → Command Palette…: the menus' commands, found by typing.""" 

278 if self._command_palette is None: 

279 recent = [str(t) for t in self.settings.value("palette_recent", [], type=list) or []] 

280 self._command_palette = CommandPalette(self, recent) 

281 self._command_palette.recent_changed = lambda titles: self.settings.setValue("palette_recent", titles) 

282 # (Hidden: in no menu; shown only when its name is typed. See vibefpmgui.punch_card.) 

283 self._command_palette.hidden = [Command(("Punch Card",), self.punch_card_action)] 

284 if getattr(self, "_quick_open", None) is not None and self._quick_open.isVisible(): 

285 self._quick_open.close() # (one palette at a time) 

286 self._command_palette.set_icon(self.palette_action.icon()) # (in the theme's colors now) 

287 self._command_palette.open() 

288 

289 def _fill_window_style_menu(self): 

290 """View → Window Style: Automatic, then Qt's styles on this platform.""" 

291 menu = self.window_style_menu 

292 menu.clear() 

293 group = QActionGroup(menu) 

294 native = themes.native_style() 

295 auto = menu.addAction(f"Automatic ({native} for System / Light / Dark, Fusion for the colorful themes)") 

296 auto.setData(themes.AUTOMATIC) 

297 menu.addSeparator() 

298 actions = [auto] 

299 for name in themes.styles(): 

300 title = name + (" (this platform's)" if name.lower() == native.lower() else "") 

301 action = menu.addAction(title) 

302 action.setData(name) 

303 if name.lower() == "fusion": 

304 action.setStatusTip("Qt's own style, alike on every platform: the panels' dividers have a grip of " 

305 "dots, and the colorful themes' colors show throughout") 

306 elif name.lower() not in themes.PALETTE_STYLES: 306 ↛ 307line 306 didn't jump to line 307 because the condition on line 306 was never true

307 action.setStatusTip(f"{name}: the colorful themes' colors don't show on everything with it " 

308 "(it draws with the platform's own colors)") 

309 actions.append(action) 

310 chosen = (themes.chosen_style or "").lower() 

311 for action in actions: 

312 action.setCheckable(True) 

313 action.setChecked((action.data() or "").lower() == chosen) 

314 group.addAction(action) 

315 action.triggered.connect(lambda _checked=False, a=action: self.set_window_style(a.data())) 

316 

317 def set_window_style(self, name: str): 

318 """View → Window Style (themes.AUTOMATIC: the theme's).""" 

319 themes.set_window_style(name) 

320 self.settings.setValue("app_style", name) 

321 self._apply_highlight_style() 

322 for editor in self.editors(): 322 ↛ 323line 322 didn't jump to line 323 because the loop on line 322 never started

323 editor.refresh_style() 

324 

325 def set_theme(self, key: str): 

326 """View → Theme: the whole window's colors (and, with Syntax Colors 

327 on Automatic, the editor's).""" 

328 themes.apply(key) 

329 self.settings.setValue("app_theme", themes.current) 

330 for k, action in self.theme_actions.items(): 

331 action.setChecked(k == themes.current) 

332 self._apply_highlight_style() # (the automatic syntax style may change with it) 

333 for editor in self.editors(): 

334 editor.refresh_style() 

335 

336 def set_syntax_style(self, name: str): 

337 hl.chosen_style = name 

338 self.settings.setValue("syntax_style", name) 

339 self._apply_highlight_style() 

340 for editor in self.editors(): 

341 editor.refresh_style() 

342 self.review_monitor.refresh_palette() # (code in the AI's answers: the same colors) 

343 self.git_panel.refresh_palette() # (the diff's colors too) 

344 

345 def changeEvent(self, event): 

346 if event.type() == QEvent.PaletteChange and hasattr(self, "terminal_panel"): # (the window is built) 

347 self._apply_highlight_style() 

348 self.graph_view.refresh_theme() # "Auto" theme follows the system 

349 self._update_legend() 

350 self.build_monitor.refresh_palette() 

351 self.test_monitor.refresh_palette() 

352 self.run_monitor.refresh_palette() 

353 self.review_monitor.refresh_palette() 

354 self.coverage_panel.refresh_palette() 

355 self.git_panel.refresh_palette() 

356 self.complexity_panel.refresh_palette() 

357 self.debug_panel.refresh_palette() 

358 self.outputs_view.refresh_palette() 

359 self._draw_config_icons() 

360 self.terminal_panel.refresh_colors() 

361 for editor in self.editors(): 

362 editor.refresh_style() 

363 self.project_tree.refresh_marks() 

364 self._update_command_icons() 

365 for editor in self.editors(): 

366 if getattr(editor, "close_button", None) is not None: 366 ↛ 365line 366 didn't jump to line 365 because the condition on line 366 was always true

367 self._style_close_button(editor.close_button) 

368 self.build_monitor.set_package_colors(self.graph_view.package_colors()) 

369 if self.graph: # (the details are HTML: its links' color is the palette's when it's made) 369 ↛ 371line 369 didn't jump to line 371 because the condition on line 369 was always true

370 self.show_package(self.graph_view.selected or self.graph.root) 

371 super().changeEvent(event) 

372 

373 FONT_DELTA_RANGE = (-4, 12) # points relative to the system size 

374 

375 def _set_toolbar_option(self, key: str, on: bool): 

376 self.settings.setValue(key, on) # (the app's setting, for every project: as the theme) 

377 self.apply_toolbar_look() 

378 

379 def apply_toolbar_look(self): 

380 """The toolbar's icons, small (16) or not (22, scaled with the text 

381 size), with their labels under them or not (View → Toolbar).""" 

382 if not hasattr(self, "command_toolbar"): 382 ↛ 383line 382 didn't jump to line 383 because the condition on line 382 was never true

383 return 

384 size = round((16 if self.small_icons_action.isChecked() else 22) * getattr(self, "_ui_factor", 1.0)) 

385 self.command_toolbar.setIconSize(QSize(size, size)) 

386 self.command_toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon if self.toolbar_labels_action.isChecked() 

387 else Qt.ToolButtonIconOnly) 

388 

389 def set_font_delta(self, delta: int, announce: bool = True): 

390 """Make all text in the window ``delta`` points bigger (or smaller) 

391 than the system size.""" 

392 lo, hi = self.FONT_DELTA_RANGE 

393 self.font_delta = max(lo, min(hi, int(delta))) 

394 base = self._base_font.pointSizeF() 

395 size = base + self.font_delta 

396 factor = size / base 

397 set_ui_scale(factor) 

398 

399 font = QFont(self._base_font) 

400 font.setPointSizeF(size) 

401 QApplication.setFont(font) 

402 # Widgets with their own fonts don't follow the application font. 

403 mono = self._mono_font() 

404 for view in (self.console, self.test_output): 

405 view.setFont(mono) 

406 code = self._mono_font(editor=True) # View → Editor Font 

407 for editor in self.editors(): 

408 editor.set_font(code) 

409 if hasattr(self, "terminal_panel"): 409 ↛ 411line 409 didn't jump to line 411 because the condition on line 409 was always true

410 self.terminal_panel.set_font(code) 

411 if hasattr(self, "git_panel"): 411 ↛ 413line 411 didn't jump to line 413 because the condition on line 411 was always true

412 self.git_panel.set_code_font(code) 

413 if hasattr(self, "complexity_panel"): 413 ↛ 415line 413 didn't jump to line 415 because the condition on line 413 was always true

414 self.complexity_panel.set_code_font(code) 

415 self._ui_factor = factor 

416 self.apply_toolbar_look() 

417 self.build_monitor.refresh_fonts() 

418 self.test_monitor.refresh_fonts() 

419 self.run_monitor.refresh_fonts() 

420 self.review_monitor.refresh_fonts() 

421 self.coverage_panel.refresh_fonts() 

422 self.git_panel.refresh_fonts() 

423 self.complexity_panel.refresh_fonts() 

424 for tree in self.result_trees: 

425 tree.mono = mono 

426 tree.doItemsLayout() 

427 self.debug_panel.breakpoints.mono = mono 

428 self.debug_panel.breakpoints.viewport().update() 

429 self.graph_view.refresh_fonts() 

430 if self.graph: 

431 self.show_package(self.graph_view.selected or self.graph.root) 

432 

433 self.bigger_action.setEnabled(self.font_delta < hi) 

434 self.smaller_action.setEnabled(self.font_delta > lo) 

435 self.reset_font_action.setEnabled(self.font_delta != 0) 

436 self.settings.setValue("font_delta", self.font_delta) 

437 if announce: 

438 self.statusBar().showMessage(f"Text size {round(factor * 100)}%", 3000) 

439 

440 def _mono_font(self, editor: bool = False) -> QFont: 

441 base = self._base_font.pointSizeF() 

442 return monospace_font((base + self.font_delta) / base, editor=editor) 

443 

444 def choose_editor_font(self): 

445 """View → Editor Font…: pick a fixed-width family and size (at 100% 

446 text size; Bigger/Smaller Text still scale it).""" 

447 current = monospace_font(1.0, editor=True) 

448 # Qt's own dialog: the macOS font panel ignores the fixed-width filter. 

449 result = QFontDialog.getFont(current, self, "Editor Font", 

450 QFontDialog.MonospacedFonts | QFontDialog.DontUseNativeDialog) 

451 # PySide returns (ok, font), PyQt (font, ok). 

452 ok, font = result if isinstance(result[0], bool) else (result[1], result[0]) 

453 if ok: 

454 self.set_editor_font(font.family(), font.pointSizeF()) 

455 

456 def set_editor_font(self, family: str, size: float): 

457 system = QFontDatabase.systemFont(QFontDatabase.FixedFont) 

458 if family == system.family() and abs(size - system.pointSizeF()) < 0.01: 458 ↛ 459line 458 didn't jump to line 459 because the condition on line 458 was never true

459 family, size = "", 0.0 # the same as the default 

460 editor_mod.editor_font_family, editor_mod.editor_font_size = family, size 

461 self.settings.setValue("editor_font_family", family) 

462 self.settings.setValue("editor_font_size", size) 

463 self.reset_editor_font_action.setEnabled(bool(family or size)) 

464 self.set_font_delta(self.font_delta, announce=False) # re-applies the fonts 

465 shown = f"{family or system.family()} {round(size or system.pointSizeF(), 1):g} pt" 

466 self.statusBar().showMessage(f"Editor font: {shown}", 4000) 

467 

468 def set_show_coverage(self, on: bool): 

469 CodeEditor.show_coverage = on 

470 self.settings.setValue("show_coverage", on) 

471 for editor in self.editors(): 

472 editor.editor.refresh_gutter() 

473 

474 def set_highlight_matches(self, on: bool): 

475 CodeEditor.highlight_matches = on 

476 self.settings.setValue("highlight_matches", on) 

477 for editor in self.editors(): 

478 editor.editor.update_occurrences() 

479 

480 def set_blame_enabled(self, on: bool): 

481 EditorTab.blame_enabled = on 

482 self.settings.setValue("git_blame", on) 

483 for editor in self.editors(): 

484 editor.set_blame_enabled(on) 

485 

486 def set_minimap_enabled(self, on: bool): 

487 EditorTab.minimap_enabled = on 

488 self.settings.setValue("minimap", on) 

489 for editor in self.editors(): 489 ↛ 490line 489 didn't jump to line 490 because the loop on line 489 never started

490 editor.minimap.setVisible(on) 

491 

492 def set_indentation(self, width: int | None = None, use_tabs: bool | None = None): 

493 """View → Indentation: spaces or tabs, and the width of an indent level.""" 

494 cls = CodeEditor 

495 cls.indent_width = cls.indent_width if width is None else width 

496 cls.use_tabs = cls.use_tabs if use_tabs is None else use_tabs 

497 self.settings.setValue("indent_width", cls.indent_width) 

498 self.settings.setValue("indent_tabs", cls.use_tabs) 

499 for editor in self.editors(): 

500 editor.editor.set_indentation(cls.indent_width, cls.use_tabs) 

501 editor.update_status() 

502 self._check_indent_actions() 

503 

504 def _check_indent_actions(self): 

505 cls = CodeEditor 

506 for key, action in self.indent_actions.items(): 

507 # (bool keys are spaces/tabs; ints are widths: True == 1 and False == 0 never collide with them) 

508 action.setChecked(key is cls.use_tabs if isinstance(key, bool) else key == cls.indent_width) 

509 

510 def set_word_wrap(self, on: bool): 

511 EditorTab.word_wrap = on 

512 self.settings.setValue("word_wrap", on) 

513 for editor in self.editors(): 

514 editor.editor.set_word_wrap(on) 

515 self.terminal_panel.set_word_wrap(on) 

516 self._set_output_wrap(on) 

517 

518 def _set_output_wrap(self, on: bool): 

519 """The Output panel's text: wrapped at its edge, or scrolled sideways.""" 

520 mode = QPlainTextEdit.WidgetWidth if on else QPlainTextEdit.NoWrap 

521 for text in (self.console, self.test_output): 

522 text.setLineWrapMode(mode) 

523 

524 def set_lsp_diagnostics_enabled(self, on: bool): 

525 EditorTab.lsp_diagnostics_enabled = on 

526 self.settings.setValue("lsp_diagnostics", on) 

527 for editor in self.editors(): 

528 editor.show_lsp_diagnostics(self._lsp_diagnostics_by_file.get(editor.path, [])) 

529 

530 def set_spelling_enabled(self, on: bool): 

531 EditorTab.spelling_enabled = on 

532 self.settings.setValue("spelling", on) 

533 for editor in self.editors(): 

534 editor.set_spelling_enabled(on) 

535 

536 def set_gremlins_enabled(self, on: bool): 

537 EditorTab.gremlins_enabled = on 

538 self.settings.setValue("gremlins", on) 

539 for editor in self.editors(): 

540 editor.set_gremlins_enabled(on) 

541 

542 def set_spelling_language(self, code: str): 

543 """View → Spelling Language: check with that language's dictionary.""" 

544 spelling.set_language(code) 

545 self.settings.setValue("spelling_language", spelling.language) 

546 for action in self.spelling_language_menu.actions(): 

547 action.setChecked(action.data() == spelling.language) 

548 for editor in self.editors(): 

549 editor.check_spelling() 

550 

551 def add_spelling_word(self, word: str): 

552 """"Add to Dictionary": not a misspelling anymore, in any file.""" 

553 spelling.user_words.add(word.lower()) 

554 self.settings.setValue("spelling_words", " ".join(sorted(spelling.user_words))) 

555 for editor in self.editors(): 

556 editor.check_spelling() 

557 self.statusBar().showMessage(f"Added “{word}” to the dictionary", 4000) 

558 

559 def set_lint_enabled(self, on: bool): 

560 EditorTab.lint_enabled = on 

561 self.settings.setValue("lint", on) 

562 for editor in self.editors(): 

563 editor.set_lint_enabled(on) 

564 self._update_editor_actions() # (the Fix … Issues items) 

565 

566 def _build_help_menu(self): 

567 """Help menu (added last, so it is the rightmost menu).""" 

568 menu = self.menuBar().addMenu("&Help") 

569 docs = menu.addAction("fpm Documentation") 

570 docs.setStatusTip(FPM_DOCS_URL) 

571 docs.triggered.connect(lambda: QDesktopServices.openUrl(QUrl(FPM_DOCS_URL))) 

572 menu.addSeparator() 

573 about = menu.addAction("About VibeFPMGUI") 

574 about.setMenuRole(QAction.AboutRole) # macOS: moved to the application menu 

575 about.triggered.connect(self.show_about) 

576 self.about_qt_action = menu.addAction("About Qt") 

577 self.about_qt_action.setMenuRole(QAction.AboutQtRole) # macOS: moved to the application menu 

578 self.about_qt_action.setStatusTip("Qt's own About window: the Qt version and license") 

579 self.about_qt_action.triggered.connect(lambda: QMessageBox.aboutQt(self, "About Qt")) 

580 

581 def show_about(self): 

582 compiler, source = self.effective_compiler() 

583 run_dialog(AboutDialog(self._fpm_executable(), compiler, source, self))