vibefpmgui.window.view

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

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

  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:
 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():
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:
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"):
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:
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():
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:
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)
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"):
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"):
410            self.terminal_panel.set_font(code)
411        if hasattr(self, "git_panel"):
412            self.git_panel.set_code_font(code)
413        if hasattr(self, "complexity_panel"):
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:
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():
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))
class ViewMixin:
 34class ViewMixin:
 35    """The View menu and how the window looks (a part of `vibefpmgui.main_window.MainWindow`)."""
 36
 37    def _build_view_menu(self):
 38        """View menu: text size, editor options, theme and colors (Panels: _build_docks)."""
 39        # View menu: text size for the whole window.
 40        # The system font, recorded once: later windows would otherwise see the
 41        # already-enlarged application font as "normal".
 42        app = QApplication.instance()
 43        if app.property("vibefpm_base_font") is None:
 44            app.setProperty("vibefpm_base_font", QFont(app.font()))
 45        self._base_font = QFont(app.property("vibefpm_base_font"))
 46        self.font_delta = 0
 47        view_menu = self.view_menu = self.menuBar().addMenu("&View")
 48        # Every command in the menus, found by typing (see vibefpmgui.command_palette).
 49        self.palette_action = view_menu.addAction("Command Palette…")
 50        self.palette_action.setShortcut(QKeySequence("Ctrl+Shift+P"))  # (⌘⇧P on macOS)
 51        self.palette_action.setStatusTip("Find any command in the menus by typing part of its name")
 52        self.palette_action.triggered.connect(self.show_command_palette)
 53        self._command_palette: CommandPalette | None = None
 54        view_menu.addSeparator()
 55        self.bigger_action = QAction("Bigger Text", self)
 56        self.bigger_action.setShortcuts([QKeySequence(QKeySequence.ZoomIn), QKeySequence("Ctrl+=")])
 57        self.bigger_action.triggered.connect(lambda: self.set_font_delta(self.font_delta + 1))
 58        self.smaller_action = QAction("Smaller Text", self)
 59        self.smaller_action.setShortcut(QKeySequence(QKeySequence.ZoomOut))
 60        self.smaller_action.triggered.connect(lambda: self.set_font_delta(self.font_delta - 1))
 61        self.reset_font_action = QAction("Actual Size", self)  # (Ctrl+0 is Graph → Fit to window)
 62        self.reset_font_action.triggered.connect(lambda: self.set_font_delta(0))
 63        view_menu.addActions([self.bigger_action, self.smaller_action, self.reset_font_action])
 64        editor_mod.editor_font_family = self.settings.value("editor_font_family", "") or ""
 65        editor_mod.editor_font_size = self.settings.value("editor_font_size", 0.0, type=float) or 0.0
 66        self.editor_font_action = view_menu.addAction("Editor Font…")
 67        self.editor_font_action.setStatusTip("Choose the font for the editor and the fpm.toml tab")
 68        self.editor_font_action.triggered.connect(self.choose_editor_font)
 69        self.reset_editor_font_action = view_menu.addAction("Reset Editor Font")
 70        self.reset_editor_font_action.setStatusTip("Use the system's fixed-width font in the editor again")
 71        self.reset_editor_font_action.triggered.connect(lambda: self.set_editor_font("", 0.0))
 72        self.reset_editor_font_action.setEnabled(bool(editor_mod.editor_font_family
 73                                                      or editor_mod.editor_font_size))
 74        view_menu.addSeparator()
 75        self.use_editor_action = QAction("Open Files in Built-in Editor", self, checkable=True)
 76        self.use_editor_action.setChecked(self.settings.value("use_editor", True, type=bool))
 77        self.use_editor_action.toggled.connect(lambda on: self.settings.setValue("use_editor", on))
 78        self.use_editor_action.setStatusTip(
 79            "Double-clicked files open in a tab here; turn off to use your default application")
 80        view_menu.addAction(self.use_editor_action)
 81        self.lint_action = QAction("Lint Fortran Files (fortitude)", self, checkable=True)
 82        EditorTab.lint_enabled = self.settings.value("lint", True, type=bool)
 83        self.lint_action.setChecked(EditorTab.lint_enabled)
 84        self.lint_action.toggled.connect(self.set_lint_enabled)
 85        if lint.executable() is None:
 86            self.lint_action.setEnabled(False)
 87            self.lint_action.setText("Lint Fortran Files (fortitude not found)")
 88        self.lint_action.setStatusTip("Check free-form Fortran files in the editor with fortitude "
 89                                      "as you type; findings are underlined and marked in the gutter")
 90        view_menu.addAction(self.lint_action)
 91        self.lsp_diagnostics_action = QAction("Language Server Diagnostics (fortls)", self, checkable=True)
 92        EditorTab.lsp_diagnostics_enabled = self.settings.value("lsp_diagnostics", True, type=bool)
 93        self.lsp_diagnostics_action.setChecked(EditorTab.lsp_diagnostics_enabled)
 94        self.lsp_diagnostics_action.toggled.connect(self.set_lsp_diagnostics_enabled)
 95        self.lsp_diagnostics_action.setStatusTip(
 96            "Show the problems fortls finds in a Fortran file when it's opened or saved (e.g. USE after "
 97            "IMPLICIT, misplaced CONTAINS, interface mismatches), marked like fortitude's")
 98        view_menu.addAction(self.lsp_diagnostics_action)
 99        self.spelling_action = QAction("Check Spelling in Comments and Strings", self, checkable=True)
100        EditorTab.spelling_enabled = self.settings.value("spelling", True, type=bool)
101        spelling.user_words = {w for w in str(self.settings.value("spelling_words", "") or "").split()}
102        self.spelling_action.setChecked(EditorTab.spelling_enabled)
103        self.spelling_action.toggled.connect(self.set_spelling_enabled)
104        if not spelling.available():
105            self.spelling_action.setEnabled(False)
106            self.spelling_action.setText("Check Spelling (pyspellchecker not found)")
107        self.spelling_action.setStatusTip(
108            "Underline misspelled words in comments and strings (right-click one for suggestions)")
109        view_menu.addSeparator()  # (spelling: its two items together)
110        view_menu.addAction(self.spelling_action)
111        spelling.set_language(str(self.settings.value("spelling_language", "en") or "en"))
112        self.spelling_language_menu = view_menu.addMenu("Spelling Language")  # (kept: PySide would let it go)
113        group = QActionGroup(self.spelling_language_menu)
114        codes = sorted(spelling.languages(), key=lambda c: spelling.LANGUAGE_NAMES.get(c, c))
115        for code in codes:
116            action = self.spelling_language_menu.addAction(f"{spelling.LANGUAGE_NAMES.get(code, code)} ({code})")
117            action.setCheckable(True)
118            action.setChecked(code == spelling.language)
119            action.setData(code)
120            group.addAction(action)
121            action.triggered.connect(lambda _=False, c=code: self.set_spelling_language(c))
122        self.spelling_language_menu.setEnabled(bool(codes))
123        view_menu.addSeparator()
124        self.gremlins_action = QAction("Show Gremlins (Non-ASCII and Invisible Characters)", self, checkable=True)
125        EditorTab.gremlins_enabled = self.settings.value("gremlins", True, type=bool)
126        self.gremlins_action.setChecked(EditorTab.gremlins_enabled)
127        self.gremlins_action.toggled.connect(self.set_gremlins_enabled)
128        self.gremlins_action.setStatusTip(
129            "Mark lines with characters that aren't plain ASCII (accented letters, smart quotes, "
130            "non-breaking or zero-width spaces, control characters) with a gremlin in the gutter")
131        view_menu.addAction(self.gremlins_action)
132        self.minimap_action = QAction("Show Minimap", self, checkable=True)
133        EditorTab.minimap_enabled = self.settings.value("minimap", True, type=bool)
134        self.minimap_action.setChecked(EditorTab.minimap_enabled)
135        self.minimap_action.setStatusTip("A zoomed-out view of the whole file at the right of the editor")
136        self.minimap_action.toggled.connect(self.set_minimap_enabled)
137        view_menu.addAction(self.minimap_action)
138        self.blame_action = QAction("Show Git Blame", self, checkable=True)
139        EditorTab.blame_enabled = self.settings.value("git_blame", False, type=bool)
140        self.blame_action.setChecked(EditorTab.blame_enabled)
141        self.blame_action.setStatusTip("Beside the editor, each line's last commit (hash, author, date); "
142                                       "click one to open it on GitHub or GitLab")
143        self.blame_action.toggled.connect(self.set_blame_enabled)
144        view_menu.addAction(self.blame_action)
145        self.coverage_action = QAction("Show Coverage in the Editor", self, checkable=True)
146        CodeEditor.show_coverage = self.settings.value("show_coverage", True, type=bool)
147        self.coverage_action.setChecked(CodeEditor.show_coverage)
148        self.coverage_action.setStatusTip("After a test run with Build options → Coverage: green and red bars in the "
149                                          "gutter for the lines the tests ran and didn't")
150        self.coverage_action.toggled.connect(self.set_show_coverage)
151        view_menu.addAction(self.coverage_action)
152        self.matches_action = QAction("Highlight Other Occurrences of the Selection", self, checkable=True)
153        CodeEditor.highlight_matches = self.settings.value("highlight_matches", True, type=bool)
154        self.matches_action.setChecked(CodeEditor.highlight_matches)
155        self.matches_action.setStatusTip("Selecting text in the editor highlights it everywhere else in the file "
156                                         "(Match case and Whole words as set in the find bar)")
157        self.matches_action.toggled.connect(self.set_highlight_matches)
158        view_menu.addAction(self.matches_action)
159        self.word_wrap_action = QAction("Word Wrap", self, checkable=True)
160        self.word_wrap_action.setShortcut(QKeySequence("Alt+Z"))
161        EditorTab.word_wrap = self.settings.value("word_wrap", False, type=bool)
162        self.word_wrap_action.setChecked(EditorTab.word_wrap)
163        self.word_wrap_action.setStatusTip("Wrap long lines in the editor, the terminal and the output at the "
164                                           "window's edge instead of scrolling sideways")
165        self.word_wrap_action.toggled.connect(self.set_word_wrap)
166        view_menu.addAction(self.word_wrap_action)
167        # Indentation: what Tab types, and how wide tabs are shown.
168        width = self.settings.value("indent_width", 4, type=int)
169        CodeEditor.indent_width = width if width in INDENT_WIDTHS else 4
170        CodeEditor.use_tabs = self.settings.value("indent_tabs", False, type=bool)
171        self.indent_menu = view_menu.addMenu("Indentation")  # (kept: PySide would let it go)
172        self.indent_actions: dict[object, QAction] = {}
173        kinds = QActionGroup(self.indent_menu)
174        for use_tabs, text, tip in ((False, "Indent with Spaces", "Tab types spaces, to the next tab stop"),
175                                    (True, "Indent with Tabs", "Tab types a tab character")):
176            action = self.indent_menu.addAction(text)
177            action.setCheckable(True)
178            action.setStatusTip(tip)
179            action.triggered.connect(lambda _=False, t=use_tabs: self.set_indentation(use_tabs=t))
180            kinds.addAction(action)
181            self.indent_actions[use_tabs] = action
182        self.indent_menu.addSeparator()
183        widths = QActionGroup(self.indent_menu)
184        for w in INDENT_WIDTHS:
185            action = self.indent_menu.addAction(f"Width: {w}")
186            action.setCheckable(True)
187            action.setStatusTip(f"An indent level is {w} columns (tabs are shown {w} wide too)")
188            action.triggered.connect(lambda _=False, n=w: self.set_indentation(width=n))
189            widths.addAction(action)
190            self.indent_actions[w] = action
191        self._check_indent_actions()
192        view_menu.addSeparator()
193        self.terminal_action = QAction("Terminal", self)
194        # ⌃` as in VS Code (the Control key on macOS too: ⌘` switches windows).
195        self.terminal_action.setShortcut(QKeySequence("Meta+`" if sys.platform == "darwin" else "Ctrl+`"))
196        self.terminal_action.setStatusTip("Your shell, in the project's folder (Terminal tab)")
197        self.terminal_action.triggered.connect(self.show_terminal)
198        view_menu.addAction(self.terminal_action)
199        self.fortls_action = QAction("Fortran Language Server (fortls)", self, checkable=True)
200        self.fortls_action.setChecked(self.settings.value("fortls", True, type=bool))
201        self.fortls_action.toggled.connect(self.set_fortls_enabled)
202        if lsp.executable() is None:
203            self.fortls_action.setEnabled(False)
204            self.fortls_action.setText("Fortran Language Server (fortls not found)")
205        self.fortls_action.setStatusTip("Hover information, Go to Definition, the outline and "
206                                        "Go to Symbol for Fortran files, from fortls")
207        view_menu.addAction(self.fortls_action)
208        view_menu.addSeparator()
209        self.theme_menu = view_menu.addMenu("Theme")  # (kept: PySide would let it go)
210        self.theme_menu.setStatusTip("The colors of the whole window (Syntax Colors are the editor's)")
211        self.theme_actions: dict[str, QAction] = {}
212        group = QActionGroup(self.theme_menu)
213        previous = None
214        for key, theme in themes.THEMES.items():
215            kind = "native" if theme.native else "dark" if theme.dark else "light"
216            if previous is not None and kind != previous:
217                self.theme_menu.addSeparator()  # (System/Light/Dark · dark colorful · light colorful)
218            previous = kind
219            action = self.theme_menu.addAction(theme.title)
220            action.setCheckable(True)
221            action.setChecked(key == themes.current)
222            action.triggered.connect(lambda _=False, k=key: self.set_theme(k))
223            group.addAction(action)
224            self.theme_actions[key] = action
225        # The style that draws the window: the theme's, or one chosen.
226        self.window_style_menu = view_menu.addMenu("Window Style")  # (kept: PySide would let it go)
227        self.window_style_menu.setStatusTip("How buttons, tabs and the panels' dividers are drawn: the "
228                                            "theme's choice, or one of Qt's styles")
229        self.window_style_menu.aboutToShow.connect(self._fill_window_style_menu)
230        # The toolbar: small icons, labels or not (also its right-click menu: see running._build_commands).
231        self.toolbar_menu = view_menu.addMenu("Toolbar")  # (kept: PySide would let it go)
232        self.small_icons_action = self.toolbar_menu.addAction("Small Icons")
233        self.small_icons_action.setCheckable(True)
234        self.small_icons_action.setChecked(self.settings.value("toolbar_small_icons", False, type=bool))
235        self.small_icons_action.setStatusTip("Smaller icons on the toolbar (the window's text size still counts)")
236        self.toolbar_labels_action = self.toolbar_menu.addAction("Show Labels")
237        self.toolbar_labels_action.setCheckable(True)
238        self.toolbar_labels_action.setChecked(self.settings.value("toolbar_labels", True, type=bool))
239        self.toolbar_labels_action.setStatusTip("The commands' names under the toolbar's icons (off: icons only, "
240                                                "their names in the tooltips)")
241        self.small_icons_action.toggled.connect(lambda on: self._set_toolbar_option("toolbar_small_icons", on))
242        self.toolbar_labels_action.toggled.connect(lambda on: self._set_toolbar_option("toolbar_labels", on))
243        hl.chosen_style = self.settings.value("syntax_style", hl.AUTO) or hl.AUTO
244        self.style_menu = view_menu.addMenu("Syntax Colors")
245        self.style_menu.setStatusTip("The Pygments color style for the editor and fpm.toml")
246        self.style_menu.aboutToShow.connect(self._fill_style_menu)
247
248    def _apply_highlight_style(self):
249        """The chosen Pygments style, or one matching the system's light/dark mode."""
250        for tree in getattr(self, "result_trees", []):
251            tree.refresh_style()
252        if hasattr(self, "debug_panel"):
253            self.debug_panel.breakpoints.refresh_style()
254
255    def _fill_style_menu(self):
256        """View → Syntax Colors: Automatic, then every installed Pygments style."""
257        menu = self.style_menu
258        menu.clear()
259        group = QActionGroup(menu)
260        auto = menu.addAction(f"Automatic ({hl.LIGHT_STYLE} / {hl.DARK_STYLE}, following light or dark mode)")
261        auto.setData(hl.AUTO)
262        menu.addSeparator()
263        light, dark = hl.available_styles()
264        actions = [auto]
265        for title, names in (("Light Styles", light), ("Dark Styles", dark)):
266            sub = menu.addMenu(title)
267            for name in names:
268                action = sub.addAction(name)
269                action.setData(name)
270                actions.append(action)
271        for action in actions:
272            action.setCheckable(True)
273            action.setChecked(action.data() == hl.chosen_style)
274            group.addAction(action)
275            action.triggered.connect(lambda _checked=False, a=action: self.set_syntax_style(a.data()))
276
277    def show_command_palette(self):
278        """View → Command Palette…: the menus' commands, found by typing."""
279        if self._command_palette is None:
280            recent = [str(t) for t in self.settings.value("palette_recent", [], type=list) or []]
281            self._command_palette = CommandPalette(self, recent)
282            self._command_palette.recent_changed = lambda titles: self.settings.setValue("palette_recent", titles)
283            # (Hidden: in no menu; shown only when its name is typed. See vibefpmgui.punch_card.)
284            self._command_palette.hidden = [Command(("Punch Card",), self.punch_card_action)]
285        if getattr(self, "_quick_open", None) is not None and self._quick_open.isVisible():
286            self._quick_open.close()  # (one palette at a time)
287        self._command_palette.set_icon(self.palette_action.icon())  # (in the theme's colors now)
288        self._command_palette.open()
289
290    def _fill_window_style_menu(self):
291        """View → Window Style: Automatic, then Qt's styles on this platform."""
292        menu = self.window_style_menu
293        menu.clear()
294        group = QActionGroup(menu)
295        native = themes.native_style()
296        auto = menu.addAction(f"Automatic ({native} for System / Light / Dark, Fusion for the colorful themes)")
297        auto.setData(themes.AUTOMATIC)
298        menu.addSeparator()
299        actions = [auto]
300        for name in themes.styles():
301            title = name + (" (this platform's)" if name.lower() == native.lower() else "")
302            action = menu.addAction(title)
303            action.setData(name)
304            if name.lower() == "fusion":
305                action.setStatusTip("Qt's own style, alike on every platform: the panels' dividers have a grip of "
306                                    "dots, and the colorful themes' colors show throughout")
307            elif name.lower() not in themes.PALETTE_STYLES:
308                action.setStatusTip(f"{name}: the colorful themes' colors don't show on everything with it "
309                                    "(it draws with the platform's own colors)")
310            actions.append(action)
311        chosen = (themes.chosen_style or "").lower()
312        for action in actions:
313            action.setCheckable(True)
314            action.setChecked((action.data() or "").lower() == chosen)
315            group.addAction(action)
316            action.triggered.connect(lambda _checked=False, a=action: self.set_window_style(a.data()))
317
318    def set_window_style(self, name: str):
319        """View → Window Style (themes.AUTOMATIC: the theme's)."""
320        themes.set_window_style(name)
321        self.settings.setValue("app_style", name)
322        self._apply_highlight_style()
323        for editor in self.editors():
324            editor.refresh_style()
325
326    def set_theme(self, key: str):
327        """View → Theme: the whole window's colors (and, with Syntax Colors
328        on Automatic, the editor's)."""
329        themes.apply(key)
330        self.settings.setValue("app_theme", themes.current)
331        for k, action in self.theme_actions.items():
332            action.setChecked(k == themes.current)
333        self._apply_highlight_style()  # (the automatic syntax style may change with it)
334        for editor in self.editors():
335            editor.refresh_style()
336
337    def set_syntax_style(self, name: str):
338        hl.chosen_style = name
339        self.settings.setValue("syntax_style", name)
340        self._apply_highlight_style()
341        for editor in self.editors():
342            editor.refresh_style()
343        self.review_monitor.refresh_palette()  # (code in the AI's answers: the same colors)
344        self.git_panel.refresh_palette()  # (the diff's colors too)
345
346    def changeEvent(self, event):
347        if event.type() == QEvent.PaletteChange and hasattr(self, "terminal_panel"):  # (the window is built)
348            self._apply_highlight_style()
349            self.graph_view.refresh_theme()  # "Auto" theme follows the system
350            self._update_legend()
351            self.build_monitor.refresh_palette()
352            self.test_monitor.refresh_palette()
353            self.run_monitor.refresh_palette()
354            self.review_monitor.refresh_palette()
355            self.coverage_panel.refresh_palette()
356            self.git_panel.refresh_palette()
357            self.complexity_panel.refresh_palette()
358            self.debug_panel.refresh_palette()
359            self.outputs_view.refresh_palette()
360            self._draw_config_icons()
361            self.terminal_panel.refresh_colors()
362            for editor in self.editors():
363                editor.refresh_style()
364            self.project_tree.refresh_marks()
365            self._update_command_icons()
366            for editor in self.editors():
367                if getattr(editor, "close_button", None) is not None:
368                    self._style_close_button(editor.close_button)
369            self.build_monitor.set_package_colors(self.graph_view.package_colors())
370            if self.graph:  # (the details are HTML: its links' color is the palette's when it's made)
371                self.show_package(self.graph_view.selected or self.graph.root)
372        super().changeEvent(event)
373
374    FONT_DELTA_RANGE = (-4, 12)  # points relative to the system size
375
376    def _set_toolbar_option(self, key: str, on: bool):
377        self.settings.setValue(key, on)  # (the app's setting, for every project: as the theme)
378        self.apply_toolbar_look()
379
380    def apply_toolbar_look(self):
381        """The toolbar's icons, small (16) or not (22, scaled with the text
382        size), with their labels under them or not (View → Toolbar)."""
383        if not hasattr(self, "command_toolbar"):
384            return
385        size = round((16 if self.small_icons_action.isChecked() else 22) * getattr(self, "_ui_factor", 1.0))
386        self.command_toolbar.setIconSize(QSize(size, size))
387        self.command_toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon if self.toolbar_labels_action.isChecked()
388                                                else Qt.ToolButtonIconOnly)
389
390    def set_font_delta(self, delta: int, announce: bool = True):
391        """Make all text in the window ``delta`` points bigger (or smaller)
392        than the system size."""
393        lo, hi = self.FONT_DELTA_RANGE
394        self.font_delta = max(lo, min(hi, int(delta)))
395        base = self._base_font.pointSizeF()
396        size = base + self.font_delta
397        factor = size / base
398        set_ui_scale(factor)
399
400        font = QFont(self._base_font)
401        font.setPointSizeF(size)
402        QApplication.setFont(font)
403        # Widgets with their own fonts don't follow the application font.
404        mono = self._mono_font()
405        for view in (self.console, self.test_output):
406            view.setFont(mono)
407        code = self._mono_font(editor=True)  # View → Editor Font
408        for editor in self.editors():
409            editor.set_font(code)
410        if hasattr(self, "terminal_panel"):
411            self.terminal_panel.set_font(code)
412        if hasattr(self, "git_panel"):
413            self.git_panel.set_code_font(code)
414        if hasattr(self, "complexity_panel"):
415            self.complexity_panel.set_code_font(code)
416        self._ui_factor = factor
417        self.apply_toolbar_look()
418        self.build_monitor.refresh_fonts()
419        self.test_monitor.refresh_fonts()
420        self.run_monitor.refresh_fonts()
421        self.review_monitor.refresh_fonts()
422        self.coverage_panel.refresh_fonts()
423        self.git_panel.refresh_fonts()
424        self.complexity_panel.refresh_fonts()
425        for tree in self.result_trees:
426            tree.mono = mono
427            tree.doItemsLayout()
428        self.debug_panel.breakpoints.mono = mono
429        self.debug_panel.breakpoints.viewport().update()
430        self.graph_view.refresh_fonts()
431        if self.graph:
432            self.show_package(self.graph_view.selected or self.graph.root)
433
434        self.bigger_action.setEnabled(self.font_delta < hi)
435        self.smaller_action.setEnabled(self.font_delta > lo)
436        self.reset_font_action.setEnabled(self.font_delta != 0)
437        self.settings.setValue("font_delta", self.font_delta)
438        if announce:
439            self.statusBar().showMessage(f"Text size {round(factor * 100)}%", 3000)
440
441    def _mono_font(self, editor: bool = False) -> QFont:
442        base = self._base_font.pointSizeF()
443        return monospace_font((base + self.font_delta) / base, editor=editor)
444
445    def choose_editor_font(self):
446        """View → Editor Font…: pick a fixed-width family and size (at 100%
447        text size; Bigger/Smaller Text still scale it)."""
448        current = monospace_font(1.0, editor=True)
449        # Qt's own dialog: the macOS font panel ignores the fixed-width filter.
450        result = QFontDialog.getFont(current, self, "Editor Font",
451                                     QFontDialog.MonospacedFonts | QFontDialog.DontUseNativeDialog)
452        # PySide returns (ok, font), PyQt (font, ok).
453        ok, font = result if isinstance(result[0], bool) else (result[1], result[0])
454        if ok:
455            self.set_editor_font(font.family(), font.pointSizeF())
456
457    def set_editor_font(self, family: str, size: float):
458        system = QFontDatabase.systemFont(QFontDatabase.FixedFont)
459        if family == system.family() and abs(size - system.pointSizeF()) < 0.01:
460            family, size = "", 0.0  # the same as the default
461        editor_mod.editor_font_family, editor_mod.editor_font_size = family, size
462        self.settings.setValue("editor_font_family", family)
463        self.settings.setValue("editor_font_size", size)
464        self.reset_editor_font_action.setEnabled(bool(family or size))
465        self.set_font_delta(self.font_delta, announce=False)  # re-applies the fonts
466        shown = f"{family or system.family()} {round(size or system.pointSizeF(), 1):g} pt"
467        self.statusBar().showMessage(f"Editor font: {shown}", 4000)
468
469    def set_show_coverage(self, on: bool):
470        CodeEditor.show_coverage = on
471        self.settings.setValue("show_coverage", on)
472        for editor in self.editors():
473            editor.editor.refresh_gutter()
474
475    def set_highlight_matches(self, on: bool):
476        CodeEditor.highlight_matches = on
477        self.settings.setValue("highlight_matches", on)
478        for editor in self.editors():
479            editor.editor.update_occurrences()
480
481    def set_blame_enabled(self, on: bool):
482        EditorTab.blame_enabled = on
483        self.settings.setValue("git_blame", on)
484        for editor in self.editors():
485            editor.set_blame_enabled(on)
486
487    def set_minimap_enabled(self, on: bool):
488        EditorTab.minimap_enabled = on
489        self.settings.setValue("minimap", on)
490        for editor in self.editors():
491            editor.minimap.setVisible(on)
492
493    def set_indentation(self, width: int | None = None, use_tabs: bool | None = None):
494        """View → Indentation: spaces or tabs, and the width of an indent level."""
495        cls = CodeEditor
496        cls.indent_width = cls.indent_width if width is None else width
497        cls.use_tabs = cls.use_tabs if use_tabs is None else use_tabs
498        self.settings.setValue("indent_width", cls.indent_width)
499        self.settings.setValue("indent_tabs", cls.use_tabs)
500        for editor in self.editors():
501            editor.editor.set_indentation(cls.indent_width, cls.use_tabs)
502            editor.update_status()
503        self._check_indent_actions()
504
505    def _check_indent_actions(self):
506        cls = CodeEditor
507        for key, action in self.indent_actions.items():
508            # (bool keys are spaces/tabs; ints are widths: True == 1 and False == 0 never collide with them)
509            action.setChecked(key is cls.use_tabs if isinstance(key, bool) else key == cls.indent_width)
510
511    def set_word_wrap(self, on: bool):
512        EditorTab.word_wrap = on
513        self.settings.setValue("word_wrap", on)
514        for editor in self.editors():
515            editor.editor.set_word_wrap(on)
516        self.terminal_panel.set_word_wrap(on)
517        self._set_output_wrap(on)
518
519    def _set_output_wrap(self, on: bool):
520        """The Output panel's text: wrapped at its edge, or scrolled sideways."""
521        mode = QPlainTextEdit.WidgetWidth if on else QPlainTextEdit.NoWrap
522        for text in (self.console, self.test_output):
523            text.setLineWrapMode(mode)
524
525    def set_lsp_diagnostics_enabled(self, on: bool):
526        EditorTab.lsp_diagnostics_enabled = on
527        self.settings.setValue("lsp_diagnostics", on)
528        for editor in self.editors():
529            editor.show_lsp_diagnostics(self._lsp_diagnostics_by_file.get(editor.path, []))
530
531    def set_spelling_enabled(self, on: bool):
532        EditorTab.spelling_enabled = on
533        self.settings.setValue("spelling", on)
534        for editor in self.editors():
535            editor.set_spelling_enabled(on)
536
537    def set_gremlins_enabled(self, on: bool):
538        EditorTab.gremlins_enabled = on
539        self.settings.setValue("gremlins", on)
540        for editor in self.editors():
541            editor.set_gremlins_enabled(on)
542
543    def set_spelling_language(self, code: str):
544        """View → Spelling Language: check with that language's dictionary."""
545        spelling.set_language(code)
546        self.settings.setValue("spelling_language", spelling.language)
547        for action in self.spelling_language_menu.actions():
548            action.setChecked(action.data() == spelling.language)
549        for editor in self.editors():
550            editor.check_spelling()
551
552    def add_spelling_word(self, word: str):
553        """"Add to Dictionary": not a misspelling anymore, in any file."""
554        spelling.user_words.add(word.lower())
555        self.settings.setValue("spelling_words", " ".join(sorted(spelling.user_words)))
556        for editor in self.editors():
557            editor.check_spelling()
558        self.statusBar().showMessage(f"Added “{word}” to the dictionary", 4000)
559
560    def set_lint_enabled(self, on: bool):
561        EditorTab.lint_enabled = on
562        self.settings.setValue("lint", on)
563        for editor in self.editors():
564            editor.set_lint_enabled(on)
565        self._update_editor_actions()  # (the Fix … Issues items)
566
567    def _build_help_menu(self):
568        """Help menu (added last, so it is the rightmost menu)."""
569        menu = self.menuBar().addMenu("&Help")
570        docs = menu.addAction("fpm Documentation")
571        docs.setStatusTip(FPM_DOCS_URL)
572        docs.triggered.connect(lambda: QDesktopServices.openUrl(QUrl(FPM_DOCS_URL)))
573        menu.addSeparator()
574        about = menu.addAction("About VibeFPMGUI")
575        about.setMenuRole(QAction.AboutRole)  # macOS: moved to the application menu
576        about.triggered.connect(self.show_about)
577        self.about_qt_action = menu.addAction("About Qt")
578        self.about_qt_action.setMenuRole(QAction.AboutQtRole)  # macOS: moved to the application menu
579        self.about_qt_action.setStatusTip("Qt's own About window: the Qt version and license")
580        self.about_qt_action.triggered.connect(lambda: QMessageBox.aboutQt(self, "About Qt"))
581
582    def show_about(self):
583        compiler, source = self.effective_compiler()
584        run_dialog(AboutDialog(self._fpm_executable(), compiler, source, self))

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

def show_command_palette(self):
277    def show_command_palette(self):
278        """View → Command Palette…: the menus' commands, found by typing."""
279        if self._command_palette is None:
280            recent = [str(t) for t in self.settings.value("palette_recent", [], type=list) or []]
281            self._command_palette = CommandPalette(self, recent)
282            self._command_palette.recent_changed = lambda titles: self.settings.setValue("palette_recent", titles)
283            # (Hidden: in no menu; shown only when its name is typed. See vibefpmgui.punch_card.)
284            self._command_palette.hidden = [Command(("Punch Card",), self.punch_card_action)]
285        if getattr(self, "_quick_open", None) is not None and self._quick_open.isVisible():
286            self._quick_open.close()  # (one palette at a time)
287        self._command_palette.set_icon(self.palette_action.icon())  # (in the theme's colors now)
288        self._command_palette.open()

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

def set_window_style(self, name: str):
318    def set_window_style(self, name: str):
319        """View → Window Style (themes.AUTOMATIC: the theme's)."""
320        themes.set_window_style(name)
321        self.settings.setValue("app_style", name)
322        self._apply_highlight_style()
323        for editor in self.editors():
324            editor.refresh_style()

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

def set_theme(self, key: str):
326    def set_theme(self, key: str):
327        """View → Theme: the whole window's colors (and, with Syntax Colors
328        on Automatic, the editor's)."""
329        themes.apply(key)
330        self.settings.setValue("app_theme", themes.current)
331        for k, action in self.theme_actions.items():
332            action.setChecked(k == themes.current)
333        self._apply_highlight_style()  # (the automatic syntax style may change with it)
334        for editor in self.editors():
335            editor.refresh_style()

View → Theme: the whole window's colors (and, with Syntax Colors on Automatic, the editor's).

def set_syntax_style(self, name: str):
337    def set_syntax_style(self, name: str):
338        hl.chosen_style = name
339        self.settings.setValue("syntax_style", name)
340        self._apply_highlight_style()
341        for editor in self.editors():
342            editor.refresh_style()
343        self.review_monitor.refresh_palette()  # (code in the AI's answers: the same colors)
344        self.git_panel.refresh_palette()  # (the diff's colors too)
def changeEvent(self, event):
346    def changeEvent(self, event):
347        if event.type() == QEvent.PaletteChange and hasattr(self, "terminal_panel"):  # (the window is built)
348            self._apply_highlight_style()
349            self.graph_view.refresh_theme()  # "Auto" theme follows the system
350            self._update_legend()
351            self.build_monitor.refresh_palette()
352            self.test_monitor.refresh_palette()
353            self.run_monitor.refresh_palette()
354            self.review_monitor.refresh_palette()
355            self.coverage_panel.refresh_palette()
356            self.git_panel.refresh_palette()
357            self.complexity_panel.refresh_palette()
358            self.debug_panel.refresh_palette()
359            self.outputs_view.refresh_palette()
360            self._draw_config_icons()
361            self.terminal_panel.refresh_colors()
362            for editor in self.editors():
363                editor.refresh_style()
364            self.project_tree.refresh_marks()
365            self._update_command_icons()
366            for editor in self.editors():
367                if getattr(editor, "close_button", None) is not None:
368                    self._style_close_button(editor.close_button)
369            self.build_monitor.set_package_colors(self.graph_view.package_colors())
370            if self.graph:  # (the details are HTML: its links' color is the palette's when it's made)
371                self.show_package(self.graph_view.selected or self.graph.root)
372        super().changeEvent(event)
FONT_DELTA_RANGE = (-4, 12)
def apply_toolbar_look(self):
380    def apply_toolbar_look(self):
381        """The toolbar's icons, small (16) or not (22, scaled with the text
382        size), with their labels under them or not (View → Toolbar)."""
383        if not hasattr(self, "command_toolbar"):
384            return
385        size = round((16 if self.small_icons_action.isChecked() else 22) * getattr(self, "_ui_factor", 1.0))
386        self.command_toolbar.setIconSize(QSize(size, size))
387        self.command_toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon if self.toolbar_labels_action.isChecked()
388                                                else Qt.ToolButtonIconOnly)

The toolbar's icons, small (16) or not (22, scaled with the text size), with their labels under them or not (View → Toolbar).

def set_font_delta(self, delta: int, announce: bool = True):
390    def set_font_delta(self, delta: int, announce: bool = True):
391        """Make all text in the window ``delta`` points bigger (or smaller)
392        than the system size."""
393        lo, hi = self.FONT_DELTA_RANGE
394        self.font_delta = max(lo, min(hi, int(delta)))
395        base = self._base_font.pointSizeF()
396        size = base + self.font_delta
397        factor = size / base
398        set_ui_scale(factor)
399
400        font = QFont(self._base_font)
401        font.setPointSizeF(size)
402        QApplication.setFont(font)
403        # Widgets with their own fonts don't follow the application font.
404        mono = self._mono_font()
405        for view in (self.console, self.test_output):
406            view.setFont(mono)
407        code = self._mono_font(editor=True)  # View → Editor Font
408        for editor in self.editors():
409            editor.set_font(code)
410        if hasattr(self, "terminal_panel"):
411            self.terminal_panel.set_font(code)
412        if hasattr(self, "git_panel"):
413            self.git_panel.set_code_font(code)
414        if hasattr(self, "complexity_panel"):
415            self.complexity_panel.set_code_font(code)
416        self._ui_factor = factor
417        self.apply_toolbar_look()
418        self.build_monitor.refresh_fonts()
419        self.test_monitor.refresh_fonts()
420        self.run_monitor.refresh_fonts()
421        self.review_monitor.refresh_fonts()
422        self.coverage_panel.refresh_fonts()
423        self.git_panel.refresh_fonts()
424        self.complexity_panel.refresh_fonts()
425        for tree in self.result_trees:
426            tree.mono = mono
427            tree.doItemsLayout()
428        self.debug_panel.breakpoints.mono = mono
429        self.debug_panel.breakpoints.viewport().update()
430        self.graph_view.refresh_fonts()
431        if self.graph:
432            self.show_package(self.graph_view.selected or self.graph.root)
433
434        self.bigger_action.setEnabled(self.font_delta < hi)
435        self.smaller_action.setEnabled(self.font_delta > lo)
436        self.reset_font_action.setEnabled(self.font_delta != 0)
437        self.settings.setValue("font_delta", self.font_delta)
438        if announce:
439            self.statusBar().showMessage(f"Text size {round(factor * 100)}%", 3000)

Make all text in the window delta points bigger (or smaller) than the system size.

def choose_editor_font(self):
445    def choose_editor_font(self):
446        """View → Editor Font…: pick a fixed-width family and size (at 100%
447        text size; Bigger/Smaller Text still scale it)."""
448        current = monospace_font(1.0, editor=True)
449        # Qt's own dialog: the macOS font panel ignores the fixed-width filter.
450        result = QFontDialog.getFont(current, self, "Editor Font",
451                                     QFontDialog.MonospacedFonts | QFontDialog.DontUseNativeDialog)
452        # PySide returns (ok, font), PyQt (font, ok).
453        ok, font = result if isinstance(result[0], bool) else (result[1], result[0])
454        if ok:
455            self.set_editor_font(font.family(), font.pointSizeF())

View → Editor Font…: pick a fixed-width family and size (at 100% text size; Bigger/Smaller Text still scale it).

def set_editor_font(self, family: str, size: float):
457    def set_editor_font(self, family: str, size: float):
458        system = QFontDatabase.systemFont(QFontDatabase.FixedFont)
459        if family == system.family() and abs(size - system.pointSizeF()) < 0.01:
460            family, size = "", 0.0  # the same as the default
461        editor_mod.editor_font_family, editor_mod.editor_font_size = family, size
462        self.settings.setValue("editor_font_family", family)
463        self.settings.setValue("editor_font_size", size)
464        self.reset_editor_font_action.setEnabled(bool(family or size))
465        self.set_font_delta(self.font_delta, announce=False)  # re-applies the fonts
466        shown = f"{family or system.family()} {round(size or system.pointSizeF(), 1):g} pt"
467        self.statusBar().showMessage(f"Editor font: {shown}", 4000)
def set_show_coverage(self, on: bool):
469    def set_show_coverage(self, on: bool):
470        CodeEditor.show_coverage = on
471        self.settings.setValue("show_coverage", on)
472        for editor in self.editors():
473            editor.editor.refresh_gutter()
def set_highlight_matches(self, on: bool):
475    def set_highlight_matches(self, on: bool):
476        CodeEditor.highlight_matches = on
477        self.settings.setValue("highlight_matches", on)
478        for editor in self.editors():
479            editor.editor.update_occurrences()
def set_blame_enabled(self, on: bool):
481    def set_blame_enabled(self, on: bool):
482        EditorTab.blame_enabled = on
483        self.settings.setValue("git_blame", on)
484        for editor in self.editors():
485            editor.set_blame_enabled(on)
def set_minimap_enabled(self, on: bool):
487    def set_minimap_enabled(self, on: bool):
488        EditorTab.minimap_enabled = on
489        self.settings.setValue("minimap", on)
490        for editor in self.editors():
491            editor.minimap.setVisible(on)
def set_indentation(self, width: int | None = None, use_tabs: bool | None = None):
493    def set_indentation(self, width: int | None = None, use_tabs: bool | None = None):
494        """View → Indentation: spaces or tabs, and the width of an indent level."""
495        cls = CodeEditor
496        cls.indent_width = cls.indent_width if width is None else width
497        cls.use_tabs = cls.use_tabs if use_tabs is None else use_tabs
498        self.settings.setValue("indent_width", cls.indent_width)
499        self.settings.setValue("indent_tabs", cls.use_tabs)
500        for editor in self.editors():
501            editor.editor.set_indentation(cls.indent_width, cls.use_tabs)
502            editor.update_status()
503        self._check_indent_actions()

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

def set_word_wrap(self, on: bool):
511    def set_word_wrap(self, on: bool):
512        EditorTab.word_wrap = on
513        self.settings.setValue("word_wrap", on)
514        for editor in self.editors():
515            editor.editor.set_word_wrap(on)
516        self.terminal_panel.set_word_wrap(on)
517        self._set_output_wrap(on)
def set_lsp_diagnostics_enabled(self, on: bool):
525    def set_lsp_diagnostics_enabled(self, on: bool):
526        EditorTab.lsp_diagnostics_enabled = on
527        self.settings.setValue("lsp_diagnostics", on)
528        for editor in self.editors():
529            editor.show_lsp_diagnostics(self._lsp_diagnostics_by_file.get(editor.path, []))
def set_spelling_enabled(self, on: bool):
531    def set_spelling_enabled(self, on: bool):
532        EditorTab.spelling_enabled = on
533        self.settings.setValue("spelling", on)
534        for editor in self.editors():
535            editor.set_spelling_enabled(on)
def set_gremlins_enabled(self, on: bool):
537    def set_gremlins_enabled(self, on: bool):
538        EditorTab.gremlins_enabled = on
539        self.settings.setValue("gremlins", on)
540        for editor in self.editors():
541            editor.set_gremlins_enabled(on)
def set_spelling_language(self, code: str):
543    def set_spelling_language(self, code: str):
544        """View → Spelling Language: check with that language's dictionary."""
545        spelling.set_language(code)
546        self.settings.setValue("spelling_language", spelling.language)
547        for action in self.spelling_language_menu.actions():
548            action.setChecked(action.data() == spelling.language)
549        for editor in self.editors():
550            editor.check_spelling()

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

def add_spelling_word(self, word: str):
552    def add_spelling_word(self, word: str):
553        """"Add to Dictionary": not a misspelling anymore, in any file."""
554        spelling.user_words.add(word.lower())
555        self.settings.setValue("spelling_words", " ".join(sorted(spelling.user_words)))
556        for editor in self.editors():
557            editor.check_spelling()
558        self.statusBar().showMessage(f"Added “{word}” to the dictionary", 4000)

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

def set_lint_enabled(self, on: bool):
560    def set_lint_enabled(self, on: bool):
561        EditorTab.lint_enabled = on
562        self.settings.setValue("lint", on)
563        for editor in self.editors():
564            editor.set_lint_enabled(on)
565        self._update_editor_actions()  # (the Fix … Issues items)
def show_about(self):
582    def show_about(self):
583        compiler, source = self.effective_compiler()
584        run_dialog(AboutDialog(self._fpm_executable(), compiler, source, self))