From 1d97f9b48b668bfc718bf5913d785ed30785375b Mon Sep 17 00:00:00 2001 From: pasta Date: Sat, 3 Oct 2026 22:51:36 -0500 Subject: [PATCH 1/6] fix(qt): register RPC console font-size shortcuts only once setButtonIcons() re-runs on every theme change (and once more from the constructor via reloadThemedWidgets()), and each run created another QShortcut for the secondary Ctrl+= / Ctrl+_ keys through GUIUtil::AddButtonShortcut(). Duplicate shortcuts for the same key in the same window are ambiguous, so Qt stops delivering them. Register the console font-size shortcuts once in the constructor instead; the keys and their window context are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/qt/rpcconsole.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/qt/rpcconsole.cpp b/src/qt/rpcconsole.cpp index aa19de5109a7..64656a1dae52 100644 --- a/src/qt/rpcconsole.cpp +++ b/src/qt/rpcconsole.cpp @@ -553,6 +553,16 @@ RPCConsole::RPCConsole(interfaces::Node& node, QWidget* parent, Qt::WindowFlags setButtonIcons(); + // Not in setButtonIcons(): it re-runs on theme changes and would stack duplicate QShortcuts. + //: Main shortcut to increase the RPC console font size. + ui->fontBiggerButton->setShortcut(tr("Ctrl++")); + //: Secondary shortcut to increase the RPC console font size. + GUIUtil::AddButtonShortcut(ui->fontBiggerButton, tr("Ctrl+=")); + //: Main shortcut to decrease the RPC console font size. + ui->fontSmallerButton->setShortcut(tr("Ctrl+-")); + //: Secondary shortcut to decrease the RPC console font size. + GUIUtil::AddButtonShortcut(ui->fontSmallerButton, tr("Ctrl+_")); + // Install event filter for up and down arrow ui->lineEdit->installEventFilter(this); ui->lineEdit->setMaxLength(16 * 1024 * 1024); @@ -1342,16 +1352,7 @@ void RPCConsole::setButtonIcons() GUIUtil::setIcon(ui->clearButton, "remove", GUIUtil::ThemedColor::RED, consoleButtonsSize); GUIUtil::setIcon(ui->fontBiggerButton, "fontbigger", GUIUtil::ThemedColor::BLUE, consoleButtonsSize); - //: Main shortcut to increase the RPC console font size. - ui->fontBiggerButton->setShortcut(tr("Ctrl++")); - //: Secondary shortcut to increase the RPC console font size. - GUIUtil::AddButtonShortcut(ui->fontBiggerButton, tr("Ctrl+=")); - GUIUtil::setIcon(ui->fontSmallerButton, "fontsmaller", GUIUtil::ThemedColor::BLUE, consoleButtonsSize); - //: Main shortcut to decrease the RPC console font size. - ui->fontSmallerButton->setShortcut(tr("Ctrl+-")); - //: Secondary shortcut to decrease the RPC console font size. - GUIUtil::AddButtonShortcut(ui->fontSmallerButton, tr("Ctrl+_")); } void RPCConsole::reloadThemedWidgets() From d5f2e4606e5aae6238775afd22fc0cf4d471a81c Mon Sep 17 00:00:00 2001 From: pasta Date: Sat, 3 Oct 2026 22:51:54 -0500 Subject: [PATCH 2/6] feat(qt): add global zoom shortcuts Add Zoom In, Zoom Out and Reset Zoom actions to the Window menu, bound to QKeySequence::ZoomIn / ZoomOut plus Ctrl+= / Ctrl+_, and Ctrl+0. They step the global font scale by 3 within GUIUtil::FONT_SCALE_MIN/MAX (-50..100; much lower shrinks text towards zero size), or reset it to the default, apply it live through GUIUtil::updateFonts(), recompute the toolbar width and persist it like the Appearance tab's font scale. With -nosettings there is no settings file to write to (writing would throw), so the zoom then lasts for the session only. The Appearance slider takes its range from the same bounds, widening it from -30..30 so it can show every scale the shortcuts can reach. The actions stay disabled until a client model is set (and again once it is cleared), and while -font-scale is given on the command line, matching the Appearance slider. The RPC console keeps its own font-size shortcuts: - Without a wallet the console is the main window's central widget and already binds the zoom-in/out keys, so the global zoom-in/out actions get no key bindings there; the menu entries still zoom the whole UI. - On macOS the main window's menu bar stays active while the separate console window has focus and would grab Cmd++, so the zoom-in/out actions resize the console text when the console window is active. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/qt/appearancewidget.cpp | 1 + src/qt/bitcoingui.cpp | 75 ++++++++++++++++++++++++++++++++ src/qt/bitcoingui.h | 6 +++ src/qt/forms/appearancewidget.ui | 4 +- src/qt/guiutil.h | 3 ++ 5 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/qt/appearancewidget.cpp b/src/qt/appearancewidget.cpp index 7cba5922dccd..5e87a1dfef85 100644 --- a/src/qt/appearancewidget.cpp +++ b/src/qt/appearancewidget.cpp @@ -88,6 +88,7 @@ AppearanceWidget::AppearanceWidget(QWidget* parent) : prevWeightBoldArg{GUIUtil::currentWeightArg(GUIUtil::FontWeight::Bold)} { ui->setupUi(this); + ui->fontScaleSlider->setRange(GUIUtil::FONT_SCALE_MIN, GUIUtil::FONT_SCALE_MAX); for (const QString& entry : GUIUtil::listThemes()) { ui->theme->addItem(entry, QVariant(entry)); diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index 0ad815bd8bcf..d22616348588 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -74,6 +74,7 @@ #include #include +#include #include namespace { @@ -88,6 +89,8 @@ constexpr int GOV_CYCLE_FRAME_MS{STATUSBAR_ICON_CYCLE_MS / (GOV_CYCLE_FRAME_COUN // Per-frame interval for the spinner animation constexpr int SPINNER_FRAME_MS{STATUSBAR_ICON_CYCLE_MS / SPINNER_FRAMES}; + +constexpr int FONT_SCALE_SHORTCUT_STEP{3}; } // anonymous namespace BitcoinGUI::BitcoinGUI(interfaces::Node& node, const NetworkStyle* networkStyle, QWidget* parent) : @@ -514,12 +517,50 @@ void BitcoinGUI::createActions() m_mask_values_action->setStatusTip(tr("Mask the values in the Overview tab")); m_mask_values_action->setCheckable(true); + m_zoom_in_action = new QAction(tr("Zoom &In"), this); + m_zoom_in_action->setStatusTip(tr("Increase the font size")); + + m_zoom_out_action = new QAction(tr("Zoom &Out"), this); + m_zoom_out_action->setStatusTip(tr("Decrease the font size")); + + // Without a wallet the RPC console is the central widget and already binds these keys to its own font size + if (walletFrame) { + m_zoom_in_action->setShortcuts({QKeySequence(QKeySequence::ZoomIn), QKeySequence(tr("Ctrl+="))}); + m_zoom_out_action->setShortcuts({QKeySequence(QKeySequence::ZoomOut), QKeySequence(tr("Ctrl+_"))}); + } + + m_zoom_reset_action = new QAction(tr("Reset &Zoom"), this); + m_zoom_reset_action->setShortcut(QKeySequence(tr("Ctrl+0"))); + m_zoom_reset_action->setStatusTip(tr("Reset the font size to default")); + + // Enabled by setClientModel() once the font scale can be persisted + for (QAction* action : {m_zoom_in_action, m_zoom_out_action, m_zoom_reset_action}) { + action->setEnabled(false); + } + connect(quitAction, &QAction::triggered, this, &BitcoinGUI::quitRequested); connect(aboutAction, &QAction::triggered, this, &BitcoinGUI::aboutClicked); connect(aboutQtAction, &QAction::triggered, qApp, QApplication::aboutQt); connect(optionsAction, &QAction::triggered, this, &BitcoinGUI::optionsClicked); connect(showHelpMessageAction, &QAction::triggered, this, &BitcoinGUI::showHelpMessageClicked); connect(showCoinJoinHelpAction, &QAction::triggered, this, &BitcoinGUI::showCoinJoinHelpClicked); + // The macOS menu bar keeps these shortcuts live while the separate RPC console window has focus, + // so let them resize the console text there like its own shortcuts do on other platforms + connect(m_zoom_in_action, &QAction::triggered, this, [this] { + if (rpcConsole->isWindow() && rpcConsole->isActiveWindow()) { + rpcConsole->fontBigger(); + } else { + adjustFontScale(FONT_SCALE_SHORTCUT_STEP); + } + }); + connect(m_zoom_out_action, &QAction::triggered, this, [this] { + if (rpcConsole->isWindow() && rpcConsole->isActiveWindow()) { + rpcConsole->fontSmaller(); + } else { + adjustFontScale(-FONT_SCALE_SHORTCUT_STEP); + } + }); + connect(m_zoom_reset_action, &QAction::triggered, this, [this] { adjustFontScale(0); }); // Jump directly to tabs in RPC-console connect(openInfoAction, &QAction::triggered, this, &BitcoinGUI::showInfo); @@ -706,6 +747,11 @@ void BitcoinGUI::createMenuBar() }); #endif + window_menu->addSeparator(); + window_menu->addAction(m_zoom_in_action); + window_menu->addAction(m_zoom_out_action); + window_menu->addAction(m_zoom_reset_action); + if (walletFrame) { #ifdef Q_OS_MACOS window_menu->addSeparator(); @@ -737,6 +783,27 @@ void BitcoinGUI::createMenuBar() help->addAction(aboutQtAction); } +void BitcoinGUI::adjustFontScale(int delta) +{ + const int current_scale{GUIUtil::fontScale()}; + const int new_scale{delta == 0 + ? GUIUtil::defaultFontScale() + : std::clamp(current_scale + delta, GUIUtil::FONT_SCALE_MIN, GUIUtil::FONT_SCALE_MAX)}; + + if (new_scale == current_scale) { + return; + } + + GUIUtil::setFontScale(new_scale); + GUIUtil::updateFonts(); + updateWidth(); + + // With -nosettings there is no settings file to write, so the zoom only lasts for this session + if (clientModel && clientModel->getOptionsModel() && gArgs.GetSettingsPath()) { + clientModel->getOptionsModel()->setOption(OptionsModel::FontScale, new_scale); + } +} + void BitcoinGUI::createToolBars() { #ifdef ENABLE_WALLET @@ -926,6 +993,11 @@ void BitcoinGUI::setClientModel(ClientModel *_clientModel, interfaces::BlockAndH unitDisplayControl->setOptionsModel(optionsModel); m_mask_values_action->setChecked(optionsModel->getOption(OptionsModel::OptionID::MaskValues).toBool()); + const bool font_scale_overridden{optionsModel->isOptionOverridden("-font-scale")}; + for (QAction* action : {m_zoom_in_action, m_zoom_out_action, m_zoom_reset_action}) { + action->setEnabled(!font_scale_overridden); + } + connect(optionsModel, &OptionsModel::displayUnitChanged, this, [this]() { m_last_gov_cycle_height.reset(); updateGovernanceCycleIcon(); }); connect(optionsModel, &OptionsModel::showCoinJoinChanged, this, &BitcoinGUI::updateCoinJoinVisibility); connect(optionsModel, &OptionsModel::showGovernanceChanged, this, &BitcoinGUI::updateGovernanceVisibility); @@ -955,6 +1027,9 @@ void BitcoinGUI::setClientModel(ClientModel *_clientModel, interfaces::BlockAndH } #endif // ENABLE_WALLET unitDisplayControl->setOptionsModel(nullptr); + for (QAction* action : {m_zoom_in_action, m_zoom_out_action, m_zoom_reset_action}) { + action->setEnabled(false); + } #ifdef Q_OS_MACOS if(dockIconMenu) diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index 6243619b05d5..a4f03a65dd9c 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -187,6 +187,9 @@ class BitcoinGUI : public QMainWindow QAction* m_mask_values_action{nullptr}; QAction* m_migrate_wallet_action{nullptr}; QMenu* m_migrate_wallet_menu{nullptr}; + QAction* m_zoom_in_action{nullptr}; + QAction* m_zoom_out_action{nullptr}; + QAction* m_zoom_reset_action{nullptr}; QComboBox* m_wallet_selector = nullptr; @@ -262,6 +265,9 @@ class BitcoinGUI : public QMainWindow /** Update UI with latest network info from model. */ void updateNetworkState(); + /** Apply a global font scale delta or reset when delta is 0. */ + void adjustFontScale(int delta); + /** Regenerate all pre-cached governance clock pixmaps (e.g. after a theme change). */ void refreshGovernanceCycleIcons(); diff --git a/src/qt/forms/appearancewidget.ui b/src/qt/forms/appearancewidget.ui index 955e94d21969..208d42259a21 100644 --- a/src/qt/forms/appearancewidget.ui +++ b/src/qt/forms/appearancewidget.ui @@ -183,10 +183,10 @@ - -30 + -50 - 30 + 100 10 diff --git a/src/qt/guiutil.h b/src/qt/guiutil.h index 4d62592b3b16..bad6841d20af 100644 --- a/src/qt/guiutil.h +++ b/src/qt/guiutil.h @@ -212,6 +212,9 @@ namespace GUIUtil /** Known fonts and their "selectable in UI" flag, in registration order. */ const std::vector>& knownFonts(); + /** Supported font scale range; much below the minimum text becomes unreadably small. */ + constexpr int FONT_SCALE_MIN{-50}; + constexpr int FONT_SCALE_MAX{100}; void setFontScale(int font_scale); int fontScale(); From 02897a50649bdfeedde031796f7fc4e592cda544 Mon Sep 17 00:00:00 2001 From: pasta Date: Sat, 3 Oct 2026 22:51:59 -0500 Subject: [PATCH 3/6] fix(qt): clamp CLI -font-scale minimum to -50 to match GUI bounds -font-scale accepted -100..100, but -100 scales every font by 1 + (-100 * 0.01) = 0, making all text invisible, and the Appearance slider and zoom shortcuts stop at -50. Validate the option and describe it in its help text using GUIUtil::FONT_SCALE_MIN/MAX, so values below -50 are now rejected at startup. Move the startup validation into GUIUtil::setFontScaleFromArg() so tests can drive the same check GuiMain() runs. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/qt/bitcoin.cpp | 15 +++++---------- src/qt/guiutil.h | 3 +++ src/qt/guiutil_font.cpp | 10 ++++++++++ 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/qt/bitcoin.cpp b/src/qt/bitcoin.cpp index 61c4e825ea43..940ff2006855 100644 --- a/src/qt/bitcoin.cpp +++ b/src/qt/bitcoin.cpp @@ -493,7 +493,7 @@ static void SetupUIArgs(ArgsManager& argsman) if (selectable) selectable_fonts.push_back(name); } argsman.AddArg("-font-family", QObject::tr("Set the font family. Possible values: %1. (default: %2)").arg(Join(selectable_fonts, ", ")).arg(GUIUtil::defaultFontFamily()).toStdString(), ArgsManager::ALLOW_ANY, OptionsCategory::GUI); - argsman.AddArg("-font-scale", QObject::tr("Set a scale factor which gets applied to the base font size. Possible range %1 (smallest fonts) to %2 (largest fonts). (default: %3)").arg(-100).arg(100).arg(GUIUtil::defaultFontScale()).toStdString(), ArgsManager::ALLOW_ANY, OptionsCategory::GUI); + argsman.AddArg("-font-scale", QObject::tr("Set a scale factor which gets applied to the base font size. Possible range %1 (smallest fonts) to %2 (largest fonts). (default: %3)").arg(GUIUtil::FONT_SCALE_MIN).arg(GUIUtil::FONT_SCALE_MAX).arg(GUIUtil::defaultFontScale()).toStdString(), ArgsManager::ALLOW_ANY, OptionsCategory::GUI); argsman.AddArg("-font-weight-bold", QObject::tr("Set the font weight for bold texts. Possible range %1 to %2 (default: %3)").arg(0).arg(8).arg(GUIUtil::defaultWeightArg(GUIUtil::FontWeight::Bold)).toStdString(), ArgsManager::ALLOW_ANY, OptionsCategory::GUI); argsman.AddArg("-font-weight-normal", QObject::tr("Set the font weight for normal texts. Possible range %1 to %2 (default: %3)").arg(0).arg(8).arg(GUIUtil::defaultWeightArg(GUIUtil::FontWeight::Normal)).toStdString(), ArgsManager::ALLOW_ANY, OptionsCategory::GUI); argsman.AddArg("-lang=", QObject::tr("Set language, for example \"de_DE\" (default: system locale)").toStdString(), ArgsManager::ALLOW_ANY, OptionsCategory::GUI); @@ -720,15 +720,10 @@ int GuiMain(int argc, char* argv[]) } } // Validate/set font scale - if (gArgs.IsArgSet("-font-scale")) { - const int nScaleMin = -100, nScaleMax = 100; - int nScale = gArgs.GetIntArg("-font-scale", GUIUtil::fontScale()); - if (nScale < nScaleMin || nScale > nScaleMax) { - QMessageBox::critical(nullptr, PACKAGE_NAME, - QObject::tr("Error: Specified font-scale invalid. Valid range %1 to %2.").arg(nScaleMin).arg(nScaleMax)); - return EXIT_FAILURE; - } - GUIUtil::setFontScale(nScale); + if (!GUIUtil::setFontScaleFromArg()) { + QMessageBox::critical(nullptr, PACKAGE_NAME, + QObject::tr("Error: Specified font-scale invalid. Valid range %1 to %2.").arg(GUIUtil::FONT_SCALE_MIN).arg(GUIUtil::FONT_SCALE_MAX)); + return EXIT_FAILURE; } // Apply font changes GUIUtil::updateFonts(); diff --git a/src/qt/guiutil.h b/src/qt/guiutil.h index bad6841d20af..6d7f19423e1d 100644 --- a/src/qt/guiutil.h +++ b/src/qt/guiutil.h @@ -217,6 +217,9 @@ namespace GUIUtil constexpr int FONT_SCALE_MAX{100}; void setFontScale(int font_scale); int fontScale(); + /** Apply `-font-scale` from the command line, config file or settings.json. + * Returns false if it is out of range (no state change in that case). */ + bool setFontScaleFromArg(); /* Weight operations expressed as caller-friendly arg ints 0..8 -- the format used by * `-font-weight-*` CLI args and QSettings persistence. */ diff --git a/src/qt/guiutil_font.cpp b/src/qt/guiutil_font.cpp index db86c3b28cc0..9ca09aa699a3 100644 --- a/src/qt/guiutil_font.cpp +++ b/src/qt/guiutil_font.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -580,6 +581,15 @@ const std::vector>& knownFonts() { return g_fonts_known void setFontScale(int font_scale) { g_font_registry.SetFontScale(font_scale); } int fontScale() { return g_font_registry.GetFontScale(); } +bool setFontScaleFromArg() +{ + if (!gArgs.IsArgSet("-font-scale")) return true; + const int64_t font_scale{gArgs.GetIntArg("-font-scale", fontScale())}; + if (font_scale < FONT_SCALE_MIN || font_scale > FONT_SCALE_MAX) return false; + setFontScale(static_cast(font_scale)); + return true; +} + int currentWeightArg(FontWeight slot) { return weightToArg(slot == FontWeight::Bold ? g_font_registry.GetWeightBold() From 6ce936338b4c9ad61023c0c7ccd6fc32d3e8e56c Mon Sep 17 00:00:00 2001 From: pasta Date: Sat, 3 Oct 2026 19:32:20 -0500 Subject: [PATCH 4/6] fix(qt): clamp the persisted font scale to the supported range OptionsModel::Init() applied the persisted font-scale setting unchecked, so a value outside -50..100 (e.g. from a hand-edited settings.json) could shrink text to nothing at -100, and it made the zoom shortcuts jump to the range boundary on their first step instead of moving by one step. Clamp it on load and, when the value came from settings.json, write the clamped value back. GuiMain() validates -font-scale again after OptionsModel::Init() through gArgs, which also sees settings.json, so without the write-back a saved -80 would still abort startup with "Specified font-scale invalid". Command-line and config-file values keep the strict validation: they are not written back, so a config value is never shadowed by settings.json and an out-of-range one is still rejected. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/qt/optionsmodel.cpp | 31 +++++++++++++++---- src/qt/test/optiontests.cpp | 62 +++++++++++++++++++++++++++++++++++++ src/qt/test/optiontests.h | 1 + 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp index 12e077381cb7..fbb34d01c66c 100644 --- a/src/qt/optionsmodel.cpp +++ b/src/qt/optionsmodel.cpp @@ -34,6 +34,8 @@ #include +#include + const char *DEFAULT_GUI_PROXY_HOST = "127.0.0.1"; static QString GetDefaultProxyAddress(); @@ -115,6 +117,14 @@ static void UpdateRwSetting(interfaces::Node& node, OptionsModel::OptionID optio } } +//! Whether settings.json holds the setting, which then takes precedence over the config file. +static bool IsSettingsFileValue(const std::string& name) +{ + bool found{false}; + gArgs.LockSettings([&](const util::Settings& settings) { found = settings.rw_settings.count(name) > 0; }); + return found; +} + //! Convert enabled/size values to bitcoin -prune setting. static util::SettingsValue PruneSetting(bool prune_enabled, int prune_size_gb) { @@ -278,15 +288,24 @@ bool OptionsModel::Init(bilingual_str& error) } // Font Scale - if (node().isSettingIgnored("font-scale")) { + const bool font_scale_overridden{node().isSettingIgnored("font-scale")}; + if (font_scale_overridden) { addOverriddenOption("-font-scale"); } + int64_t font_scale; + try { + font_scale = SettingToInt(node().getPersistentSetting("font-scale"), GUIUtil::defaultFontScale()); + } catch (const std::exception& e) { + return setting_error("font-scale", e); + } + const int clamped_scale{static_cast(std::clamp(font_scale, GUIUtil::FONT_SCALE_MIN, GUIUtil::FONT_SCALE_MAX))}; + // Save a clamped settings.json value so the -font-scale validation at startup accepts it; + // an out-of-range config file value is left for that validation to reject. + if (clamped_scale != font_scale && !font_scale_overridden && IsSettingsFileValue("font-scale")) { + UpdateRwSetting(node(), FontScale, /*suffix=*/"", clamped_scale); + } if (GUIUtil::fontsLoaded()) { - try { - GUIUtil::setFontScale(SettingToInt(node().getPersistentSetting("font-scale"), GUIUtil::defaultFontScale())); - } catch (const std::exception& e) { - return setting_error("font-scale", e); - } + GUIUtil::setFontScale(clamped_scale); } // Font Weight (Normal) diff --git a/src/qt/test/optiontests.cpp b/src/qt/test/optiontests.cpp index 626b5d1079d1..7f73ea4e2185 100644 --- a/src/qt/test/optiontests.cpp +++ b/src/qt/test/optiontests.cpp @@ -23,6 +23,7 @@ #include #include +#include OptionTests::OptionTests(interfaces::Node& node) : m_node(node) { @@ -263,3 +264,64 @@ void OptionTests::updateFontsWithPixelSizedWidget() GUIUtil::updateFonts(); QCOMPARE(label->font().pointSizeF(), scaled_size); } + +void OptionTests::fontScaleSources() +{ + const int previous_scale{GUIUtil::fontScale()}; + const auto read_settings_file{[] { + std::ifstream file{gArgs.GetDataDirNet() / "settings.json"}; + return std::string{std::istreambuf_iterator{file}, std::istreambuf_iterator{}}; + }}; + + const auto set_sources{[](const util::SettingsValue& settings_file, const util::SettingsValue& command_line, const util::SettingsValue& config_file) { + gArgs.LockSettings([&](util::Settings& settings) { + settings.rw_settings.erase("font-scale"); + settings.command_line_options.erase("font-scale"); + settings.ro_config[""].erase("font-scale"); + if (!settings_file.isNull()) settings.rw_settings["font-scale"] = settings_file; + if (!command_line.isNull()) settings.command_line_options["font-scale"] = {command_line}; + if (!config_file.isNull()) settings.ro_config[""]["font-scale"] = {config_file}; + }); + gArgs.WriteSettingsFile(); + }}; + + const UniValue none{}; + struct Case { + UniValue settings_file, command_line, config_file; + bool accepted; + int scale; + //! Expected settings.json value afterwards, nullptr if absent. + const char* saved; + }; + // OptionsModel writes the font scale as a string, older settings may hold a number. + for (const auto& [settings_file, command_line, config_file, accepted, scale, saved] : { + // An out-of-range settings.json value is clamped and saved back. + Case{"-80", none, none, true, GUIUtil::FONT_SCALE_MIN, "-50"}, + Case{GUIUtil::FONT_SCALE_MAX + 1, none, none, true, GUIUtil::FONT_SCALE_MAX, "100"}, + Case{"20", none, none, true, 20, "20"}, + // Command line and config file values are validated strictly, and never saved. + Case{none, "-80", none, false, 0, nullptr}, + Case{none, none, "-80", false, 0, nullptr}, + Case{none, none, "20", true, 20, nullptr}, + // settings.json is left alone while the command line overrides it. + Case{"-80", "20", none, true, 20, "-80"}, + }) { + set_sources(settings_file, command_line, config_file); + + // GuiMain() runs OptionsModel::Init() and then validates -font-scale. + bilingual_str error; + QVERIFY(OptionsModel{m_node}.Init(error)); + QCOMPARE(GUIUtil::setFontScaleFromArg(), accepted); + if (accepted) QCOMPARE(GUIUtil::fontScale(), scale); + + const std::string settings_json{read_settings_file()}; + if (saved) { + QVERIFY(settings_json.find(strprintf("\"font-scale\": \"%s\"", saved)) != std::string::npos); + } else { + QVERIFY(settings_json.find("font-scale") == std::string::npos); + } + } + + set_sources(none, none, none); + GUIUtil::setFontScale(previous_scale); +} diff --git a/src/qt/test/optiontests.h b/src/qt/test/optiontests.h index 2cee85ab5903..c6f58970ed6e 100644 --- a/src/qt/test/optiontests.h +++ b/src/qt/test/optiontests.h @@ -27,6 +27,7 @@ private Q_SLOTS: void extractFilter(); void effectivePointSize(); void updateFontsWithPixelSizedWidget(); + void fontScaleSources(); private: interfaces::Node& m_node; From 713c3184086affc423507e84ac323acaa141fdb7 Mon Sep 17 00:00:00 2001 From: pasta Date: Sat, 3 Oct 2026 19:36:08 -0500 Subject: [PATCH 5/6] test(qt): cover the zoom shortcuts in AppTests Zoom in, zoom out and press Reset Zoom on the real main window and check that the live font scale moves and that OptionsModel persists the new value. With dynamic settings disabled (-nosettings) zooming must still work without writing the setting. Then press the zoom keys in the RPC console and check that they resize only the console text and leave the application font scale alone. Cover both layouts. With a wallet the RPC console is a separate window and the Zoom In/Out actions own the zoom keys. Without one (built with --disable-wallet or run with -disablewallet) the console is embedded in the main window and keeps those keys for its own font size, so the actions must have no shortcuts and are triggered from the menu instead; Reset Zoom stays on Ctrl+0 in both. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/qt/bitcoingui.cpp | 3 ++ src/qt/test/apptests.cpp | 84 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index d22616348588..f8e83356170a 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -518,9 +518,11 @@ void BitcoinGUI::createActions() m_mask_values_action->setCheckable(true); m_zoom_in_action = new QAction(tr("Zoom &In"), this); + m_zoom_in_action->setObjectName("zoomInAction"); m_zoom_in_action->setStatusTip(tr("Increase the font size")); m_zoom_out_action = new QAction(tr("Zoom &Out"), this); + m_zoom_out_action->setObjectName("zoomOutAction"); m_zoom_out_action->setStatusTip(tr("Decrease the font size")); // Without a wallet the RPC console is the central widget and already binds these keys to its own font size @@ -530,6 +532,7 @@ void BitcoinGUI::createActions() } m_zoom_reset_action = new QAction(tr("Reset &Zoom"), this); + m_zoom_reset_action->setObjectName("zoomResetAction"); m_zoom_reset_action->setShortcut(QKeySequence(tr("Ctrl+0"))); m_zoom_reset_action->setStatusTip(tr("Reset the font size to default")); diff --git a/src/qt/test/apptests.cpp b/src/qt/test/apptests.cpp index 2bdf82f7e7f6..38dc8107fae0 100644 --- a/src/qt/test/apptests.cpp +++ b/src/qt/test/apptests.cpp @@ -13,6 +13,8 @@ #include #include #include +#include +#include #include #include @@ -28,6 +30,8 @@ #include #include +#include + namespace { //! Regex find a string group inside of the console output QString FindInConsole(const QString& output, const QString& pattern) @@ -51,6 +55,84 @@ void TestRpcCommand(RPCConsole* console) const QString pattern = QStringLiteral("\"chain\": \"(\\w+)\""); QCOMPARE(FindInConsole(output, pattern), QString("regtest")); } + +//! Zoom the main window in, out and back to the default, checking the live and persisted font scale. +void TestZoomShortcuts(BitcoinGUI* window, interfaces::Node& node) +{ + QAction* zoom_in{window->findChild("zoomInAction")}; + QAction* zoom_out{window->findChild("zoomOutAction")}; + QAction* zoom_reset{window->findChild("zoomResetAction")}; + QVERIFY(zoom_in && zoom_out && zoom_reset); + QVERIFY(zoom_in->isEnabled()); + window->activateWindow(); + QVERIFY(QTest::qWaitForWindowActive(window)); + + // Without a wallet the RPC console is embedded and keeps the zoom in/out keys for its own font size. + const bool console_embedded{!window->findChild()->isWindow()}; + QCOMPARE(zoom_in->shortcuts().isEmpty(), console_embedded); + QCOMPARE(zoom_out->shortcuts().isEmpty(), console_embedded); + + const auto persisted_scale{[&] { return SettingToInt(node.getPersistentSetting("font-scale")).value_or(INT64_MIN); }}; + + const auto zoom{[&](QAction* action, QKeySequence::StandardKey key) { + if (console_embedded) { + action->trigger(); + } else { + QTest::keySequence(window, key); + } + }}; + + const int initial_scale{GUIUtil::fontScale()}; + zoom(zoom_in, QKeySequence::ZoomIn); + const int zoomed_scale{GUIUtil::fontScale()}; + QVERIFY(zoomed_scale > initial_scale); + QCOMPARE(persisted_scale(), int64_t{zoomed_scale}); + + zoom(zoom_in, QKeySequence::ZoomIn); + QVERIFY(GUIUtil::fontScale() > zoomed_scale); + zoom(zoom_out, QKeySequence::ZoomOut); + QCOMPARE(GUIUtil::fontScale(), zoomed_scale); + QCOMPARE(persisted_scale(), int64_t{zoomed_scale}); + + QTest::keySequence(window, QKeySequence{"Ctrl+0"}); + QCOMPARE(GUIUtil::fontScale(), GUIUtil::defaultFontScale()); + QCOMPARE(persisted_scale(), int64_t{GUIUtil::defaultFontScale()}); + + // With -nosettings there is no settings file, so the zoom applies for the session without being saved. + node.forceSetting("settings", false); + zoom(zoom_in, QKeySequence::ZoomIn); + QVERIFY(GUIUtil::fontScale() > GUIUtil::defaultFontScale()); + QCOMPARE(persisted_scale(), int64_t{GUIUtil::defaultFontScale()}); + QTest::keySequence(window, QKeySequence{"Ctrl+0"}); + QCOMPARE(GUIUtil::fontScale(), GUIUtil::defaultFontScale()); + node.forceSetting("settings", {}); + + // OptionTests compares the whole settings.json later on. + node.updateRwSetting("font-scale", {}); +} + +//! The zoom keys in the RPC console, separate or embedded, resize only the console text. +void TestConsoleZoomShortcuts(RPCConsole* console) +{ + console->activateWindow(); + QVERIFY(QTest::qWaitForWindowActive(console->window())); + QLineEdit* line_edit{console->findChild("lineEdit")}; + const auto console_font_size{[] { return QSettings().value("consoleFontSize").toInt(); }}; + + // Start from a fixed size: without platform fonts (the "minimal" plugin on Linux) the default size is 0, + // which the console rejects, and the keys cannot move it from there. + const int initial_size{12}; + console->setFontSize(initial_size); + QCOMPARE(console_font_size(), initial_size); + + const int app_scale{GUIUtil::fontScale()}; + // The font size buttons animate their click, so the size changes asynchronously. + QTest::keySequence(line_edit, QKeySequence{"Ctrl++"}); + QTRY_COMPARE(console_font_size(), initial_size + 1); + QTest::keySequence(line_edit, QKeySequence{"Ctrl+-"}); + QTRY_COMPARE(console_font_size(), initial_size); + QCOMPARE(GUIUtil::fontScale(), app_scale); +} } // namespace //! Entry point for BitcoinApplication tests. @@ -94,6 +176,7 @@ void AppTests::appTests() void AppTests::guiTests(BitcoinGUI* window) { HandleCallback callback{"guiTests", *this}; + TestZoomShortcuts(window, m_app.node()); connect(window, &BitcoinGUI::consoleShown, this, &AppTests::consoleTests); expectCallback("consoleTests"); QAction* action = window->findChild("openRPCConsoleAction"); @@ -105,6 +188,7 @@ void AppTests::consoleTests(RPCConsole* console) { HandleCallback callback{"consoleTests", *this}; TestRpcCommand(console); + TestConsoleZoomShortcuts(console); } //! Destructor to shut down after the last expected callback completes. From cbf65bb7f5a7c9979f393ad504fd125ce8d03c3a Mon Sep 17 00:00:00 2001 From: pasta Date: Sat, 3 Oct 2026 19:46:32 -0500 Subject: [PATCH 6/6] doc: add release notes for zoom shortcuts and font scale range Co-Authored-By: Claude Opus 5.5 (1M context) --- doc/release-notes-7216.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 doc/release-notes-7216.md diff --git a/doc/release-notes-7216.md b/doc/release-notes-7216.md new file mode 100644 index 000000000000..16ce6bd6d2f7 --- /dev/null +++ b/doc/release-notes-7216.md @@ -0,0 +1,19 @@ +GUI changes +----------- + +- The Window menu has new Zoom In, Zoom Out and Reset Zoom entries that change + the font scale of the whole application, bound to the platform zoom + shortcuts (Ctrl++ / Ctrl+= and Ctrl+- / Ctrl+_; Cmd on macOS) and Ctrl+0. + The new scale is saved like the Appearance tab's font scale setting. Like + that slider, the entries are disabled when `-font-scale` is given on the + command line. Inside the RPC console window the zoom-in/out keys still resize + only the console text, and when the console is the main window + (`-disablewallet` or builds without wallet support) the keys stay with the + console while the menu entries keep zooming the whole application. (#7216) + +- The font scale range is now -50 to 100, both for `-font-scale` and the + Appearance tab slider, which used to stop at -30 and 30. `-font-scale` values + below -50 on the command line or in the configuration file, which earlier + versions accepted down to -100 although they shrank text towards zero size, + are now rejected at startup. A font scale saved in `settings.json` + outside the range is clamped on load and the clamped value is saved. (#7216)