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
« 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.
3Each option is saved in the settings and applied to every open editor.
4"""
6from __future__ import annotations
8import sys
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
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
33class ViewMixin:
34 """The View menu and how the window looks (a part of `vibefpmgui.main_window.MainWindow`)."""
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)
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()
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()))
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()
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()))
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()
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()
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)
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)
373 FONT_DELTA_RANGE = (-4, 12) # points relative to the system size
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()
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)
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)
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)
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)
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)
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())
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)
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()
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()
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)
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)
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()
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)
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)
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)
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, []))
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)
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)
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()
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)
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)
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"))
581 def show_about(self):
582 compiler, source = self.effective_compiler()
583 run_dialog(AboutDialog(self._fpm_executable(), compiler, source, self))