From 2bba8374802c0bcfa17178c8223872dbd7e49511 Mon Sep 17 00:00:00 2001 From: Warchamp7 Date: Tue, 28 Jan 2025 15:53:01 -0500 Subject: [PATCH] frontend: Add new appearance options --- frontend/OBSApp.cpp | 3 + frontend/OBSApp_Themes.cpp | 198 +++++++++---- frontend/components/AbsoluteSlider.cpp | 68 +++++ frontend/components/AbsoluteSlider.hpp | 12 + frontend/data/themes/Yami.obt | 191 +++++++------ frontend/data/themes/Yami_Classic.ovt | 23 +- frontend/forms/OBSBasicSettings.ui | 262 +++++++++++++++++- frontend/settings/OBSBasicSettings.cpp | 6 + frontend/settings/OBSBasicSettings.hpp | 3 + .../settings/OBSBasicSettings_Appearance.cpp | 53 ++++ frontend/utility/OBSTheme.hpp | 3 + frontend/utility/OBSThemeVariable.hpp | 2 + 12 files changed, 660 insertions(+), 164 deletions(-) diff --git a/frontend/OBSApp.cpp b/frontend/OBSApp.cpp index 7eb52bd9a37122..068135594defe7 100644 --- a/frontend/OBSApp.cpp +++ b/frontend/OBSApp.cpp @@ -296,6 +296,9 @@ void OBSApp::InitUserConfigDefaults() config_set_default_bool(userConfig, "BasicWindow", "MultiviewDrawAreas", true); config_set_default_bool(userConfig, "BasicWindow", "MediaControlsCountdownTimer", true); + + config_set_default_int(userConfig, "Appearance", "FontScale", 10); + config_set_default_int(userConfig, "Appearance", "Density", 1); } static bool do_mkdir(const char *path) diff --git a/frontend/OBSApp_Themes.cpp b/frontend/OBSApp_Themes.cpp index b4206bb66823b1..d91f069cca4aaf 100644 --- a/frontend/OBSApp_Themes.cpp +++ b/frontend/OBSApp_Themes.cpp @@ -204,7 +204,7 @@ static QColor ParseColor(CFParser &cfp) return res; } -static bool ParseCalc(CFParser &cfp, QStringList &calc, vector &vars) +static bool ParseMath(CFParser &cfp, QStringList &values, vector &vars) { int ret = cf_next_token_should_be(cfp, "(", ";", nullptr); if (ret != PARSE_SUCCESS) @@ -216,36 +216,44 @@ static bool ParseCalc(CFParser &cfp, QStringList &calc, vector if (cf_token_is(cfp, ";")) break; - if (cf_token_is(cfp, "calc")) { - /* Internal calc's do not have proper names. + if (cf_token_is(cfp, "calc") || cf_token_is(cfp, "max") || cf_token_is(cfp, "min")) { + /* Internal math operations do not have proper names. * They are anonymous variables */ OBSThemeVariable var; - QStringList subcalc; + QStringList subvalues; var.name = QString("__unnamed_%1").arg(QRandomGenerator::global()->generate64()); - if (!ParseCalc(cfp, subcalc, vars)) + OBSThemeVariable::VariableType varType; + if (cf_token_is(cfp, "calc")) + varType = OBSThemeVariable::Calc; + else if (cf_token_is(cfp, "max")) + varType = OBSThemeVariable::Max; + else if (cf_token_is(cfp, "min")) + varType = OBSThemeVariable::Min; + + if (!ParseMath(cfp, subvalues, vars)) return false; - var.type = OBSThemeVariable::Calc; - var.value = subcalc; - calc << var.name; + var.type = varType; + var.value = subvalues; + values << var.name; vars.push_back(std::move(var)); } else if (cf_token_is(cfp, "var")) { QString value; if (!ParseVarName(cfp, value)) return false; - calc << value; + values << value; } else { - calc << QString::fromUtf8(cfp->cur_token->str.array, cfp->cur_token->str.len); + values << QString::fromUtf8(cfp->cur_token->str.array, cfp->cur_token->str.len); } if (!cf_next_token(cfp)) return false; } - return !calc.isEmpty(); + return !values.isEmpty(); } static vector ParseThemeVariables(const char *themeData) @@ -316,6 +324,11 @@ static vector ParseThemeVariables(const char *themeData) if (!cf_next_token(cfp)) return vars; + /* Special values passed to the theme by OBS are prefixed with 'obs' so we + * prevent theme variables from using it as a prefix */ + if (key.startsWith("obs")) + continue; + if (cfp->cur_token->type == CFTOKEN_NUM) { const char *ch = cfp->cur_token->str.array; const char *end = ch + cfp->cur_token->str.len; @@ -348,14 +361,20 @@ static vector ParseThemeVariables(const char *themeData) var.value = value; var.type = OBSThemeVariable::Alias; - } else if (cf_token_is(cfp, "calc")) { - QStringList calc; + } else if (cf_token_is(cfp, "calc") || cf_token_is(cfp, "max") || cf_token_is(cfp, "min")) { + QStringList values; - if (!ParseCalc(cfp, calc, vars)) + if (cf_token_is(cfp, "calc")) + var.type = OBSThemeVariable::Calc; + else if (cf_token_is(cfp, "max")) + var.type = OBSThemeVariable::Max; + else if (cf_token_is(cfp, "min")) + var.type = OBSThemeVariable::Min; + + if (!ParseMath(cfp, values, vars)) continue; - var.type = OBSThemeVariable::Calc; - var.value = calc; + var.value = values; } else { var.type = OBSThemeVariable::String; BPtr strVal = cf_literal_to_str(cfp->cur_token->str.array, cfp->cur_token->str.len); @@ -367,8 +386,9 @@ static vector ParseThemeVariables(const char *themeData) if (cf_token_is(cfp, "!") && cf_next_token_should_be(cfp, "editable", nullptr, nullptr) == PARSE_SUCCESS) { - if (var.type == OBSThemeVariable::Calc || var.type == OBSThemeVariable::Alias) { - blog(LOG_WARNING, "Variable of calc/alias type cannot be editable: %s", + if (var.type == OBSThemeVariable::Calc || var.type == OBSThemeVariable::Max || + var.type == OBSThemeVariable::Min || var.type == OBSThemeVariable::Alias) { + blog(LOG_WARNING, "Math or alias variable type cannot be editable: %s", QT_TO_UTF8(var.name)); } else { var.editable = true; @@ -496,10 +516,10 @@ static bool ResolveVariable(const QHash &vars, OBSThe return true; } -static QString EvalCalc(const QHash &vars, const OBSThemeVariable &var, - const int recursion = 0); +static QString EvalMath(const QHash &vars, const OBSThemeVariable &var, + const OBSThemeVariable::VariableType type, const int recursion = 0); -static OBSThemeVariable ParseCalcVariable(const QHash &vars, const QString &value, +static OBSThemeVariable ParseMathVariable(const QHash &vars, const QString &value, const int recursion = 0) { OBSThemeVariable var; @@ -527,15 +547,17 @@ static OBSThemeVariable ParseCalcVariable(const QHash var.value = value; ResolveVariable(vars, var); - /* Handle nested calc()s */ - if (var.type == OBSThemeVariable::Calc) { - QString val = EvalCalc(vars, var, recursion + 1); - var = ParseCalcVariable(vars, val); + /* Handle nested math calculations */ + if (var.type == OBSThemeVariable::Calc || var.type == OBSThemeVariable::Max || + var.type == OBSThemeVariable::Min) { + QString val = EvalMath(vars, var, var.type, recursion + 1); + var = ParseMathVariable(vars, val); } /* Only number or size would be valid here */ if (var.type != OBSThemeVariable::Number && var.type != OBSThemeVariable::Size) { - blog(LOG_ERROR, "calc() operand is not a size or number: %s", QT_TO_UTF8(var.value.toString())); + blog(LOG_ERROR, "Math operand is not a size or number: %s %s %d", QT_TO_UTF8(var.name), + QT_TO_UTF8(var.value.toString()), var.type); throw invalid_argument("Operand not of numeric type"); } } @@ -543,69 +565,87 @@ static OBSThemeVariable ParseCalcVariable(const QHash return var; } -static QString EvalCalc(const QHash &vars, const OBSThemeVariable &var, const int recursion) +static QString EvalMath(const QHash &vars, const OBSThemeVariable &var, + const OBSThemeVariable::VariableType type, const int recursion) { if (recursion >= 10) { /* Abort after 10 levels of recursion */ - blog(LOG_ERROR, "Maximum calc() recursion levels hit!"); + blog(LOG_ERROR, "Maximum recursion levels hit!"); return "'Invalid expression'"; } - QStringList args = var.value.toStringList(); - if (args.length() != 3) { - blog(LOG_ERROR, "calc() had invalid number of arguments: %lld (%s)", args.length(), - QT_TO_UTF8(args.join(", "))); + if (type != OBSThemeVariable::Calc && type != OBSThemeVariable::Max && type != OBSThemeVariable::Min) { + blog(LOG_ERROR, "Invalid type for math operation!"); return "'Invalid expression'"; } + QStringList args = var.value.toStringList(); QString &opt = args[1]; - if (opt != '*' && opt != '+' && opt != '-' && opt != '/') { + if (type == OBSThemeVariable::Calc && (opt != '*' && opt != '+' && opt != '-' && opt != '/')) { blog(LOG_ERROR, "Unknown/invalid calc() operator: %s", QT_TO_UTF8(opt)); return "'Invalid expression'"; } + if ((type == OBSThemeVariable::Max || type == OBSThemeVariable::Min) && opt != ',') { + blog(LOG_ERROR, "Invalid math separator: %s", QT_TO_UTF8(opt)); + return "'Invalid expression'"; + } + + if (args.length() != 3) { + blog(LOG_ERROR, "Math parse had invalid number of arguments: %lld (%s)", args.length(), + QT_TO_UTF8(args.join(", "))); + return "'Invalid expression'"; + } + OBSThemeVariable val1, val2; try { - val1 = ParseCalcVariable(vars, args[0], recursion); - val2 = ParseCalcVariable(vars, args[2], recursion); + val1 = ParseMathVariable(vars, args[0], 0); + val2 = ParseMathVariable(vars, args[2], 0); } catch (...) { return "'Invalid expression'"; } /* Ensure that suffixes match (if any) */ if (!val1.suffix.isEmpty() && !val2.suffix.isEmpty() && val1.suffix != val2.suffix) { - blog(LOG_ERROR, "calc() requires suffixes to match or only one to be present! %s != %s", + blog(LOG_ERROR, "Math operation requires suffixes to match or only one to be present! %s != %s", QT_TO_UTF8(val1.suffix), QT_TO_UTF8(val2.suffix)); return "'Invalid expression'"; } - double val = numeric_limits::quiet_NaN(); double d1 = val1.userValue.isValid() ? val1.userValue.toDouble() : val1.value.toDouble(); double d2 = val2.userValue.isValid() ? val2.userValue.toDouble() : val2.value.toDouble(); if (!isfinite(d1) || !isfinite(d2)) { blog(LOG_ERROR, - "calc() received at least one invalid value:" + "At least one invalid math value:" " op1: %f, op2: %f", d1, d2); return "'Invalid expression'"; } - if (opt == "+") - val = d1 + d2; - else if (opt == "-") - val = d1 - d2; - else if (opt == "*") - val = d1 * d2; - else if (opt == "/") - val = d1 / d2; + double val = numeric_limits::quiet_NaN(); - if (!isnormal(val)) { - blog(LOG_ERROR, - "Invalid calc() math resulted in non-normal number:" - " %f %s %f = %f", - d1, QT_TO_UTF8(opt), d2, val); - return "'Invalid expression'"; + if (type == OBSThemeVariable::Calc) { + if (opt == "+") + val = d1 + d2; + else if (opt == "-") + val = d1 - d2; + else if (opt == "*") + val = d1 * d2; + else if (opt == "/") + val = d1 / d2; + + if (!isnormal(val)) { + blog(LOG_ERROR, + "Invalid calc() resulted in non-normal number:" + " %f %s %f = %f", + d1, QT_TO_UTF8(opt), d2, val); + return "'Invalid expression'"; + } + } else if (type == OBSThemeVariable::Max) { + val = d1 > d2 ? d1 : d2; + } else if (type == OBSThemeVariable::Min) { + val = d1 < d2 ? d1 : d2; } bool isInteger = ceill(val) == val; @@ -661,8 +701,9 @@ static QString PrepareQSS(const QHash &vars, const QS if (var.type == OBSThemeVariable::Color) { replace = value.value().name(QColor::HexRgb); - } else if (var.type == OBSThemeVariable::Calc) { - replace = EvalCalc(vars, var); + } else if (var.type == OBSThemeVariable::Calc || var.type == OBSThemeVariable::Max || + var.type == OBSThemeVariable::Min) { + replace = EvalMath(vars, var, var.type); } else if (var.type == OBSThemeVariable::Size || var.type == OBSThemeVariable::Number) { double val = value.toDouble(); bool isInteger = ceill(val) == val; @@ -747,6 +788,23 @@ static QPalette PreparePalette(const QHash &vars, con return pal; } +static double getPaddingForDensityId(int id) +{ + double paddingValue = 4; + + if (id == -2) { + paddingValue = 0.25; + } else if (id == -3) { + paddingValue = 2; + } else if (id == -4) { + paddingValue = 4; + } else if (id == -5) { + paddingValue = 6; + } + + return paddingValue; +} + OBSTheme *OBSApp::GetTheme(const QString &name) { if (!themes.contains(name)) @@ -775,6 +833,22 @@ bool OBSApp::SetTheme(const QString &name) QStringList themeIds(theme->dependencies); themeIds << theme->id; + /* Inject Appearance settings into theme vars */ + OBSThemeVariable fontScale; + fontScale.name = "obsFontScale"; + fontScale.type = OBSThemeVariable::Number; + fontScale.value = QVariant::fromValue(config_get_int(App()->GetUserConfig(), "Appearance", "FontScale")); + + const int density = config_get_int(App()->GetUserConfig(), "Appearance", "Density"); + + OBSThemeVariable padding; + padding.name = "obsPadding"; + padding.type = OBSThemeVariable::Number; + padding.value = QVariant::fromValue(getPaddingForDensityId(density)); + + vars[fontScale.name] = std::move(fontScale); + vars[padding.name] = std::move(padding); + /* Find and add high contrast adjustment layer if available */ if (HighContrastEnabled()) { for (const OBSTheme &theme_ : themes) { @@ -805,6 +879,22 @@ bool OBSApp::SetTheme(const QString &name) contents.emplaceBack(content.constData()); } + /* Check if OBS appearance settings are used in the theme */ + currentTheme->usesFontScale = false; + currentTheme->usesDensity = false; + for (const OBSThemeVariable &var_ : vars) { + if (var_.type != OBSThemeVariable::Alias) + continue; + + if (var_.value.toString() == "obsFontScale") { + currentTheme->usesFontScale = true; + } + + if (var_.value.toString() == "obsPadding") { + currentTheme->usesDensity = true; + } + } + const QString stylesheet = PrepareQSS(vars, contents); const QPalette palette = PreparePalette(vars, defaultPalette); setPalette(palette); diff --git a/frontend/components/AbsoluteSlider.cpp b/frontend/components/AbsoluteSlider.cpp index 9f2e766565527d..12eec4898426f9 100644 --- a/frontend/components/AbsoluteSlider.cpp +++ b/frontend/components/AbsoluteSlider.cpp @@ -1,16 +1,23 @@ #include "AbsoluteSlider.hpp" + +#include + #include "moc_AbsoluteSlider.cpp" AbsoluteSlider::AbsoluteSlider(QWidget *parent) : SliderIgnoreScroll(parent) { installEventFilter(this); setMouseTracking(true); + + tickColor.setRgb(0x5b, 0x62, 0x73); } AbsoluteSlider::AbsoluteSlider(Qt::Orientation orientation, QWidget *parent) : SliderIgnoreScroll(orientation, parent) { installEventFilter(this); setMouseTracking(true); + + tickColor.setRgb(0x5b, 0x62, 0x73); } void AbsoluteSlider::mousePressEvent(QMouseEvent *event) @@ -96,3 +103,64 @@ int AbsoluteSlider::posToRangeValue(QMouseEvent *event) return sliderValue; } + +bool AbsoluteSlider::getDisplayTicks() const +{ + return displayTicks; +} + +void AbsoluteSlider::setDisplayTicks(bool display) +{ + displayTicks = display; +} + +QColor AbsoluteSlider::getTickColor() const +{ + return tickColor; +} + +void AbsoluteSlider::setTickColor(QColor c) +{ + tickColor = std::move(c); +} + +void AbsoluteSlider::paintEvent(QPaintEvent *event) +{ + if (!getDisplayTicks()) { + QSlider::paintEvent(event); + return; + } + + QPainter painter(this); + + QStyleOptionSlider opt; + initStyleOption(&opt); + + QRect groove = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); + QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); + + const bool isHorizontal = orientation() == Qt::Horizontal; + + const int sliderLength = isHorizontal ? groove.width() - handle.width() : groove.height() - handle.height(); + const int handleSize = isHorizontal ? handle.width() : handle.height(); + const int grooveSize = isHorizontal ? groove.height() : groove.width(); + const int grooveStart = isHorizontal ? groove.left() : groove.top(); + const int tickLinePos = isHorizontal ? groove.center().y() : groove.center().x(); + const int tickLength = std::max((int)(grooveSize * 1.5) + grooveSize, 8 + grooveSize); + const int tickLineStart = tickLinePos - (tickLength / 2) + 1; + + for (double offset = minimum(); offset <= maximum(); offset += singleStep()) { + double tickPercent = (offset - minimum()) / (maximum() - minimum()); + const int tickLineOffset = grooveStart + std::floor(sliderLength * tickPercent) + (handleSize / 2); + + const int xPos = isHorizontal ? tickLineOffset : tickLineStart; + const int yPos = isHorizontal ? tickLineStart : tickLineOffset; + + const int tickWidth = isHorizontal ? 1 : tickLength; + const int tickHeight = isHorizontal ? tickLength : 1; + + painter.fillRect(xPos, yPos, tickWidth, tickHeight, tickColor); + } + + QSlider::paintEvent(event); +} diff --git a/frontend/components/AbsoluteSlider.hpp b/frontend/components/AbsoluteSlider.hpp index 8345037feae9b3..f4dd0fa795178d 100644 --- a/frontend/components/AbsoluteSlider.hpp +++ b/frontend/components/AbsoluteSlider.hpp @@ -4,11 +4,18 @@ class AbsoluteSlider : public SliderIgnoreScroll { Q_OBJECT + Q_PROPERTY(QColor tickColor READ getTickColor WRITE setTickColor DESIGNABLE true) public: AbsoluteSlider(QWidget *parent = nullptr); AbsoluteSlider(Qt::Orientation orientation, QWidget *parent = nullptr); + QColor getTickColor() const; + void setTickColor(QColor c); + + bool getDisplayTicks() const; + void setDisplayTicks(bool display); + signals: void absoluteSliderHovered(int value); @@ -20,6 +27,11 @@ class AbsoluteSlider : public SliderIgnoreScroll { int posToRangeValue(QMouseEvent *event); + virtual void paintEvent(QPaintEvent *event) override; + private: bool dragging = false; + bool displayTicks = false; + + QColor tickColor; }; diff --git a/frontend/data/themes/Yami.obt b/frontend/data/themes/Yami.obt index de668a327790fc..8cdcbe1cfabe46 100644 --- a/frontend/data/themes/Yami.obt +++ b/frontend/data/themes/Yami.obt @@ -93,15 +93,9 @@ /* Layout */ /* Configurable Values */ - - /* TODO: Min 8, Max 12, Step 1 */ - --font_base_value: 10; - - /* TODO: Min 2, Max 7, Step 1 */ - --spacing_base_value: 4; - - /* TODO: Min 0.25, Max 10, Step 2 */ - --padding_base_value: 4; + --font_base_value: var(--obsFontScale); + --padding_base_value: var(--obsPadding); + --spacing_base_value: calc(2 + calc(var(--obsPadding) / 2)); /* TODO: Better Accessibility focus state */ /* TODO: Move Accessibilty Colors to Theme config system */ @@ -111,26 +105,27 @@ --os_mac_font_base_value: 12; --font_base: calc(1pt * var(--font_base_value)); - --font_small: calc(0.9pt * var(--font_base_value)); - --font_xsmall: calc(0.85pt * var(--font_base_value)); + --font_small: max(7pt, calc(0.8pt * var(--font_base_value))); + --font_xsmall: max(6.25pt, calc(0.85pt * var(--font_base_value))); --font_large: calc(1.1pt * var(--font_base_value)); --font_xlarge: calc(1.5pt * var(--font_base_value)); --font_heading: calc(2.5pt * var(--font_base_value)); - --icon_base: calc(6px + var(--font_base_value)); + --icon_base: calc(calc(max(2, var(--obsPadding)) * 1px) + 12px); - --spacing_base: calc(0.5px * var(--spacing_base_value)); - --spacing_large: calc(1px * var(--spacing_base_value)); - --spacing_small: calc(0.25px * var(--spacing_base_value)); + --spacing_base: min(max(1px, calc(0.4 * var(--spacing_base_value))), 2px); + --spacing_large: min(max(2px, calc(1px * var(--spacing_base_value))), 4px); + --spacing_small: max(1px, calc(0.25px * var(--spacing_base_value))); --spacing_title: 4px; --padding_base: calc(0.5px * var(--padding_base_value)); - --padding_large: calc(1px * var(--padding_base_value)); - --padding_xlarge: calc(1.75px * var(--padding_base_value)); - --padding_small: calc(0.25px * var(--padding_base_value)); + --padding_large: min(max(1px, calc(1px * var(--padding_base_value))), 5px); + --padding_xlarge: min(max(2px, calc(1.75px * var(--padding_base_value))), 10px); + --padding_small: max(0px, calc(0.25px * var(--padding_base_value))); - --padding_wide: calc(8px + calc(2 * var(--padding_base_value))); + --padding_container: max(4px, var(--padding_base)); + --padding_wide: min(calc(12px + max(var(--padding_base_value), 4)), 24px); --padding_menu: calc(4px + calc(2 * var(--padding_base_value))); --padding_base_border: calc(var(--padding_base) + 1px); @@ -154,9 +149,10 @@ --input_font_scale: calc(var(--font_base_value) * 2.2); --input_font_padding: calc(var(--padding_base_value) * 2); - --input_height_base: calc(var(--input_font_scale) + var(--input_font_padding)); - --input_padding: var(--padding_large); - --input_height: calc(var(--input_height_base) - calc(var(--input_padding) * 2)); + --input_height_base: max(calc(var(--input_font_scale) + var(--input_font_padding)), 24); + --input_padding: calc(2px + var(--padding_base)); + --input_text_padding: max(calc(6px + var(--padding_base)), 8px); + --input_height: calc(var(--input_height_base) - calc(var(--input_padding) * 2px)); --input_height_half: calc(var(--input_height_base) / 2); --input_bg: var(--grey4); @@ -196,6 +192,8 @@ --scrollbar_down: var(--grey8); --scrollbar_border: var(--grey2); + --preview_scale_width: calc(calc(var(--input_text_padding) * 3.5) * calc(var(--font_base_value) / 10)); + --separator_hover: var(--white1); --highlight: rgb(42, 130, 218); @@ -450,6 +448,9 @@ SourceTree QWidget { border: 1px solid var(--bg_base); } +* { + spacing: var(--spacing_small); +} /* Misc */ @@ -627,10 +628,11 @@ OBSDock > QWidget { border-bottom-right-radius: var(--border_radius); border: 1px solid var(--border_color); border-top: none; + } #transitionsFrame { - padding: var(--padding_large); + padding: var(--padding_container); } OBSDock QLabel { @@ -679,16 +681,15 @@ QScrollArea { * oversize it and use margin to crunch it back down */ OBSBasicStatusBar { - margin-top: 4px; + margin-top: var(--spacing_large); border-top: 1px solid var(--border_color); background: var(--bg_base); } StatusBarWidget > QFrame { - margin-top: 1px; border: 0px solid var(--border_color); border-left-width: 1px; - padding: 0px 8px 2px; + padding: 0px var(--padding_xlarge) var(--padding_small); } /* Group Box */ @@ -821,6 +822,7 @@ QToolBar { background-color: transparent; border: none; margin: var(--spacing_base) 0px; + spacing: var(--spacing_base); } QToolBarExtension { @@ -911,11 +913,10 @@ QTabBar QToolButton { QComboBox, QDateTimeEdit { background-color: var(--input_bg); - border-style: solid; border: 1px solid var(--input_bg); border-radius: var(--border_radius); - padding: var(--padding_large) var(--padding_large); - padding-left: 10px; + padding: var(--input_padding) var(--input_text_padding); + height: var(--input_height); } QComboBox QAbstractItemView::item:selected, @@ -977,8 +978,7 @@ QPlainTextEdit { background-color: var(--input_bg); border: none; border-radius: var(--border_radius); - padding: var(--input_padding) var(--padding_small) var(--input_padding) var(--input_padding); - padding-left: 8px; + padding: var(--input_padding) var(--input_text_padding); border: 1px solid var(--input_bg); height: var(--input_height); } @@ -997,6 +997,13 @@ QPlainTextEdit:focus { border-color: var(--input_border_focus); } +QLineEdit:read-only, +QLineEdit:read-only:hover, +QLineEdit:read-only:focus { + background-color: transparent; + border-color: var(--input_bg); +} + QTextEdit:!editable, QTextEdit:!editable:hover, QTextEdit:!editable:focus { @@ -1010,8 +1017,8 @@ QDoubleSpinBox { background-color: var(--input_bg); border: 1px solid var(--input_bg); border-radius: var(--border_radius); - padding: var(--input_padding) 0px var(--input_padding) var(--input_padding); - padding-left: 8px; + padding: var(--input_padding) var(--input_text_padding); + height: var(--input_height); max-height: var(--input_height); } @@ -1099,7 +1106,7 @@ QDoubleSpinBox::down-arrow { /* Controls Dock */ #controlsFrame { - padding: var(--padding_large); + padding: var(--padding_container); } #controlsFrame QPushButton { @@ -1146,17 +1153,58 @@ QDoubleSpinBox::down-arrow { /* Buttons */ QPushButton { - color: var(--text); background-color: var(--button_bg); + color: var(--text); + border: 1px solid var(--button_border); border-radius: var(--border_radius); height: var(--input_height); max-height: var(--input_height); + margin-top: var(--spacing_input); + margin-bottom: var(--spacing_input); padding: var(--input_padding) var(--padding_wide); icon-size: var(--icon_base); + outline: none; } -QPushButton { - border: 1px solid var(--button_border); +QPushButton:hover { + background-color: var(--button_bg_hover); +} + +QPushButton:hover, +QPushButton:focus { + border-color: var(--button_border_hover); +} + +QPushButton::flat { + background-color: var(--button_bg); +} + +QPushButton:checked { + background-color: var(--primary); + border-color: var(--primary_light); +} + +QPushButton:checked:hover, +QPushButton:checked:focus { + border-color: var(--primary_lighter); +} + +QPushButton:pressed, +QPushButton:pressed:hover { + background-color: var(--button_bg_down); + border-color: var(--button_border); +} + +QPushButton:disabled { + background-color: var(--button_bg_disabled); + border-color: var(--button_border); +} + +QPushButton::menu-indicator { + image: url(theme:Dark/down.svg); + subcontrol-position: right; + subcontrol-origin: padding; + width: 25px; } QToolButton { @@ -1167,7 +1215,7 @@ QToolButton, .btn-tool { background-color: var(--button_bg); padding: var(--padding_base) var(--padding_base); - margin: 0px var(--spacing_base); + margin: 0px 0px; border: 1px solid var(--button_border); border-radius: var(--border_radius); icon-size: var(--icon_base); @@ -1178,15 +1226,6 @@ QToolButton:last-child, margin-right: 0px; } -QPushButton:hover, -QPushButton:focus { - border-color: var(--button_border_hover); -} - -QPushButton:hover { - background-color: var(--button_bg_hover); -} - QToolButton:hover, QToolButton:focus, .btn-tool:hover, @@ -1197,25 +1236,6 @@ QToolButton:focus, background-color: var(--button_bg_hover); } -QPushButton::flat { - background-color: var(--button_bg); -} - -QPushButton:checked { - background-color: var(--primary); -} - -QPushButton:checked:hover, -QPushButton:checked:focus { - border-color: var(--primary_lighter); -} - -QPushButton:pressed, -QPushButton:pressed:hover { - background-color: var(--button_bg_down); - border-color: var(--button_border); -} - QToolButton:pressed, QToolButton:pressed:hover, .btn-tool:pressed, @@ -1224,24 +1244,12 @@ QToolButton:pressed:hover, border-color: var(--button_border); } -QPushButton:disabled { - background-color: var(--button_bg_disabled); - border-color: var(--button_border); -} - QToolButton:disabled, .btn-tool:disabled { background-color: var(--button_bg_disabled); border-color: transparent; } -QPushButton::menu-indicator { - image: url(theme:Dark/down.svg); - subcontrol-position: right; - subcontrol-origin: padding; - width: 25px; -} - /* Sliders */ QSlider::groove { @@ -1312,7 +1320,7 @@ QSlider::handle:hover { } QSlider::handle:pressed { - background-color: var(--white5); + background-color: var(--white3); } QSlider::handle:disabled { @@ -1352,6 +1360,15 @@ QSlider::handle:disabled { border-bottom: 1px solid #3c404b; } +VolControl { + background: var(--bg_base); +} + +VolControl QLabel { + font-size: var(--font_small); + margin: var(--spacing_small) 0px; +} + VolControl #volLabel { padding: var(--padding_base) 0px var(--padding_base); text-align: center; @@ -1380,7 +1397,7 @@ VolControl #volLabel { } #vMixerScrollArea VolControl { - padding: var(--padding_large) 0px var(--padding_base); + padding: var(--padding_container) 0px var(--padding_container); border-right: 1px solid var(--border_color); } @@ -1410,6 +1427,7 @@ VolControl #volLabel { } #vMixerScrollArea VolControl QPushButton { + margin-left: var(--spacing_base); margin-right: var(--padding_xlarge); } @@ -1417,10 +1435,6 @@ VolControl #volLabel { margin-left: var(--padding_xlarge); } -VolControl { - background: var(--bg_base); -} - VolumeMeter { background: transparent; } @@ -1539,6 +1553,7 @@ QCheckBox::indicator, QGroupBox::indicator { width: var(--icon_base); height: var(--icon_base); + margin-right: var(--spacing_large); } QGroupBox::indicator { @@ -1954,7 +1969,7 @@ OBSBasicAdvAudio #scrollAreaWidgetContents { font-size: var(--font_xsmall); height: 14px; max-height: 14px; - padding: 0px var(--padding_xlarge); + padding: 0px; margin: 0; border: none; border-radius: 0; @@ -1964,7 +1979,13 @@ OBSBasicAdvAudio #scrollAreaWidgetContents { border: 1px solid var(--grey6); } +#previewScalePercent { + padding: 0px var(--input_text_padding); + min-width: var(--preview_scale_width); +} + #previewScalingMode { + padding: 0px var(--input_text_padding); border: 1px solid var(--grey6); } diff --git a/frontend/data/themes/Yami_Classic.ovt b/frontend/data/themes/Yami_Classic.ovt index 3163765335394e..d273d103a3d755 100644 --- a/frontend/data/themes/Yami_Classic.ovt +++ b/frontend/data/themes/Yami_Classic.ovt @@ -24,23 +24,13 @@ --primary_light: rgb(33,71,109); /* Layout */ - --font_base_value: 9; - --spacing_base_value: 2; - --padding_base_value: 0.25; + --font_small: max(7pt, calc(0.5pt * var(--font_base_value))); - /* OS Fixes */ - --os_mac_font_base_value: 11; + --padding_large: min(max(0px, calc(1px * var(--padding_base_value))), 5px); - --font_small: calc(0.75pt * var(--font_base_value)); + --padding_container: max(2px, var(--padding_base)); - --icon_base: calc(6px + var(--font_base_value)); - - --padding_xlarge: calc(2px + calc(0.5px * var(--padding_base_value))); - - --padding_wide: calc(18px + calc(0.25 * var(--padding_base_value))); - --padding_menu: calc(8px + calc(1 * var(--padding_base_value))); - - --input_height_base: calc(1px + calc(var(--input_font_scale) + var(--input_font_padding))); + /* Inputs / Controls */ --border_color: var(--grey6); @@ -48,6 +38,10 @@ --border_radius_small: 1px; --border_radius_large: 2px; + --input_height_base: max(calc(var(--input_font_scale) + var(--input_font_padding)), 20); + --input_padding: calc(0px + var(--padding_base)); + --input_text_padding: max(calc(6px + var(--padding_base)), 8px); + --input_bg: var(--grey4); --input_bg_hover: var(--grey1); --input_bg_focus: var(--grey6); @@ -263,7 +257,6 @@ QPushButton[toolButton="true"] { #vMixerScrollArea QLabel { font-size: var(--font_small); - margin: var(--padding_xlarge) 0px; } #vMixerScrollArea #volLabel { diff --git a/frontend/forms/OBSBasicSettings.ui b/frontend/forms/OBSBasicSettings.ui index 50e69dd2e6e140..c1ed3217eb52e8 100644 --- a/frontend/forms/OBSBasicSettings.ui +++ b/frontend/forms/OBSBasicSettings.ui @@ -979,7 +979,7 @@ - Basic.Settings.Appearance.General + Basic.Settings.Appearance false @@ -1021,6 +1021,215 @@ + + + Font Size + + + appearanceFontScale + + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Qt::NoFocus + + + 10 + + + Qt::AlignCenter + + + true + + + false + + + + + + + + 0 + 0 + + + + 8 + + + 12 + + + 2 + + + 10 + + + Qt::Horizontal + + + QSlider::TicksBothSides + + + 1 + + + + + + + + + + Density + + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + radio-switcher + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Classic + + + true + + + true + + + radio-box + + + appearanceDensityButtonGroup + + + + + + + Compact + + + true + + + true + + + radio-box + + + appearanceDensityButtonGroup + + + + + + + Normal + + + true + + + true + + + true + + + radio-box + + + appearanceDensityButtonGroup + + + + + + + Comfortable + + + true + + + true + + + radio-box + + + appearanceDensityButtonGroup + + + + + + + Qt::Horizontal @@ -1028,7 +1237,7 @@ 170 - 0 + 10 @@ -1049,6 +1258,31 @@ + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + + + + Some appearance options are not available for this style. + + + text-warning + + + + + + @@ -2898,8 +3132,8 @@ 0 0 - 766 - 592 + 424 + 175 @@ -3304,8 +3538,8 @@ 0 0 - 766 - 558 + 509 + 371 @@ -3945,8 +4179,8 @@ 0 0 - 766 - 558 + 625 + 489 @@ -4495,8 +4729,8 @@ 0 0 - 766 - 592 + 258 + 510 @@ -8479,6 +8713,11 @@ QLineEdit
settings/OBSHotkeyEdit.hpp
+ + AbsoluteSlider + QSlider +
components/AbsoluteSlider.hpp
+
listWidget @@ -9058,4 +9297,7 @@ + + + diff --git a/frontend/settings/OBSBasicSettings.cpp b/frontend/settings/OBSBasicSettings.cpp index 1d202093d4610b..186c70e21664d3 100644 --- a/frontend/settings/OBSBasicSettings.cpp +++ b/frontend/settings/OBSBasicSettings.cpp @@ -297,6 +297,7 @@ void RestrictResetBitrates(initializer_list boxes, int maxbitrate); #define SCROLL_CHANGED &QSpinBox::valueChanged #define DSCROLL_CHANGED &QDoubleSpinBox::valueChanged #define TEXT_CHANGED &QPlainTextEdit::textChanged +#define SLIDER_CHANGED &QSlider::valueChanged #define GENERAL_CHANGED &OBSBasicSettings::GeneralChanged #define STREAM1_CHANGED &OBSBasicSettings::Stream1Changed @@ -368,6 +369,11 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) HookWidget(ui->multiviewLayout, COMBO_CHANGED, GENERAL_CHANGED); HookWidget(ui->theme, COMBO_CHANGED, APPEAR_CHANGED); HookWidget(ui->themeVariant, COMBO_CHANGED, APPEAR_CHANGED); + HookWidget(ui->appearanceFontScale, SLIDER_CHANGED, APPEAR_CHANGED); + HookWidget(ui->appearanceDensity1, CHECK_CHANGED, APPEAR_CHANGED); + HookWidget(ui->appearanceDensity2, CHECK_CHANGED, APPEAR_CHANGED); + HookWidget(ui->appearanceDensity3, CHECK_CHANGED, APPEAR_CHANGED); + HookWidget(ui->appearanceDensity4, CHECK_CHANGED, APPEAR_CHANGED); HookWidget(ui->service, COMBO_CHANGED, STREAM1_CHANGED); HookWidget(ui->server, COMBO_CHANGED, STREAM1_CHANGED); HookWidget(ui->customServer, EDIT_CHANGED, STREAM1_CHANGED); diff --git a/frontend/settings/OBSBasicSettings.hpp b/frontend/settings/OBSBasicSettings.hpp index 08664e0d7a3f3d..38412505398bf6 100644 --- a/frontend/settings/OBSBasicSettings.hpp +++ b/frontend/settings/OBSBasicSettings.hpp @@ -225,6 +225,8 @@ class OBSBasicSettings : public QDialog { /* Appearance */ void InitAppearancePage(); + void enableAppearanceFontControls(bool enable); + void enableAppearanceDensityControls(bool enable); bool IsCustomServer(); @@ -346,6 +348,7 @@ private slots: private slots: void on_theme_activated(int idx); void on_themeVariant_activated(int idx); + void updateAppearanceControls(); void on_listWidget_itemSelectionChanged(); void on_buttonBox_clicked(QAbstractButton *button); diff --git a/frontend/settings/OBSBasicSettings_Appearance.cpp b/frontend/settings/OBSBasicSettings_Appearance.cpp index 80e3fc9143034c..316ee5bf0f4513 100644 --- a/frontend/settings/OBSBasicSettings_Appearance.cpp +++ b/frontend/settings/OBSBasicSettings_Appearance.cpp @@ -21,6 +21,15 @@ void OBSBasicSettings::InitAppearancePage() ui->theme->setCurrentIndex(idx); ui->themeVariant->setPlaceholderText(QTStr("Basic.Settings.Appearance.General.NoVariant")); + + ui->appearanceFontScale->setDisplayTicks(true); + + connect(ui->appearanceFontScale, &QSlider::valueChanged, ui->appearanceFontScaleText, + [this](int value) { ui->appearanceFontScaleText->setText(QString::number(value)); }); + ui->appearanceFontScaleText->setText(QString::number(ui->appearanceFontScale->value())); + + connect(App(), &OBSApp::StyleChanged, this, &OBSBasicSettings::updateAppearanceControls); + updateAppearanceControls(); } void OBSBasicSettings::LoadThemeList(bool reload) @@ -83,6 +92,16 @@ void OBSBasicSettings::LoadAppearanceSettings(bool reload) App()->SetTheme(themeId); } + + int fontScale = config_get_int(App()->GetUserConfig(), "Appearance", "FontScale"); + ui->appearanceFontScale->setValue(fontScale); + + int densityId = config_get_int(App()->GetUserConfig(), "Appearance", "Density"); + QAbstractButton *densityButton = ui->appearanceDensityButtonGroup->button(densityId); + if (densityButton) { + densityButton->setChecked(true); + } + updateAppearanceControls(); } void OBSBasicSettings::SaveAppearanceSettings() @@ -93,6 +112,13 @@ void OBSBasicSettings::SaveAppearanceSettings() if (savedTheme != currentTheme) { config_set_string(config, "Appearance", "Theme", QT_TO_UTF8(currentTheme->id)); } + + config_set_int(config, "Appearance", "FontScale", ui->appearanceFontScale->value()); + + int densityId = ui->appearanceDensityButtonGroup->checkedId(); + config_set_int(config, "Appearance", "Density", densityId); + + App()->SetTheme(currentTheme->id); } void OBSBasicSettings::on_theme_activated(int) @@ -104,3 +130,30 @@ void OBSBasicSettings::on_themeVariant_activated(int) { LoadAppearanceSettings(true); } + +void OBSBasicSettings::updateAppearanceControls() +{ + OBSTheme *theme = App()->GetTheme(); + enableAppearanceFontControls(theme->usesFontScale); + enableAppearanceDensityControls(theme->usesDensity); + if (!theme->usesFontScale || !theme->usesDensity) { + ui->appearanceOptionsWarning->setVisible(true); + } else { + ui->appearanceOptionsWarning->setVisible(false); + } + style()->polish(ui->appearanceOptionsWarningLabel); +} + +void OBSBasicSettings::enableAppearanceFontControls(bool enable) +{ + ui->appearanceFontScale->setEnabled(enable); + ui->appearanceFontScaleText->setEnabled(enable); +} + +void OBSBasicSettings::enableAppearanceDensityControls(bool enable) +{ + foreach(QAbstractButton * button, ui->appearanceDensityButtonGroup->buttons()) + { + button->setEnabled(enable); + } +} diff --git a/frontend/utility/OBSTheme.hpp b/frontend/utility/OBSTheme.hpp index dbb3b0a6c63e46..d0e5eb99c99019 100644 --- a/frontend/utility/OBSTheme.hpp +++ b/frontend/utility/OBSTheme.hpp @@ -41,4 +41,7 @@ struct OBSTheme { bool isVisible; /* Whether it should be shown to the user */ bool isBaseTheme; /* Whether it is a "style" or variant */ bool isHighContrast; /* Whether it is a high-contrast adjustment layer */ + + bool usesFontScale = false; /* Whether the generated qss uses the font scale option */ + bool usesDensity = false; /* Whether the generated qss uses the density option */ }; diff --git a/frontend/utility/OBSThemeVariable.hpp b/frontend/utility/OBSThemeVariable.hpp index f04f90cc59feb1..b5b1cd6c824ca5 100644 --- a/frontend/utility/OBSThemeVariable.hpp +++ b/frontend/utility/OBSThemeVariable.hpp @@ -28,6 +28,8 @@ struct OBSThemeVariable { String, /* Raw string (e.g. color name, border style, etc.) */ Alias, /* Points at another variable, value will be the key */ Calc, /* Simple calculation with two operands */ + Min, /* Get the smallest of two Size or Number */ + Max, /* Get the largest of two Size or Number */ }; /* Whether the variable should be editable in the UI */