frontend: Add new appearance options

This commit is contained in:
Warchamp7 2025-01-28 15:53:01 -05:00 committed by Ryan Foster
parent cce189011e
commit c0c77071b5
12 changed files with 648 additions and 170 deletions

View File

@ -296,6 +296,9 @@ void OBSApp::InitUserConfigDefaults()
config_set_default_bool(userConfig, "BasicWindow", "MultiviewDrawAreas", true); config_set_default_bool(userConfig, "BasicWindow", "MultiviewDrawAreas", true);
config_set_default_bool(userConfig, "BasicWindow", "MediaControlsCountdownTimer", 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) static bool do_mkdir(const char *path)

View File

@ -204,7 +204,7 @@ static QColor ParseColor(CFParser &cfp)
return res; return res;
} }
static bool ParseCalc(CFParser &cfp, QStringList &calc, vector<OBSThemeVariable> &vars) static bool ParseMath(CFParser &cfp, QStringList &values, vector<OBSThemeVariable> &vars)
{ {
int ret = cf_next_token_should_be(cfp, "(", ";", nullptr); int ret = cf_next_token_should_be(cfp, "(", ";", nullptr);
if (ret != PARSE_SUCCESS) if (ret != PARSE_SUCCESS)
@ -216,36 +216,44 @@ static bool ParseCalc(CFParser &cfp, QStringList &calc, vector<OBSThemeVariable>
if (cf_token_is(cfp, ";")) if (cf_token_is(cfp, ";"))
break; break;
if (cf_token_is(cfp, "calc")) { if (cf_token_is(cfp, "calc") || cf_token_is(cfp, "max") || cf_token_is(cfp, "min")) {
/* Internal calc's do not have proper names. /* Internal math operations do not have proper names.
* They are anonymous variables */ * They are anonymous variables */
OBSThemeVariable var; OBSThemeVariable var;
QStringList subcalc; QStringList subvalues;
var.name = QString("__unnamed_%1").arg(QRandomGenerator::global()->generate64()); 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; return false;
var.type = OBSThemeVariable::Calc; var.type = varType;
var.value = subcalc; var.value = subvalues;
calc << var.name; values << var.name;
vars.push_back(std::move(var)); vars.push_back(std::move(var));
} else if (cf_token_is(cfp, "var")) { } else if (cf_token_is(cfp, "var")) {
QString value; QString value;
if (!ParseVarName(cfp, value)) if (!ParseVarName(cfp, value))
return false; return false;
calc << value; values << value;
} else { } 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)) if (!cf_next_token(cfp))
return false; return false;
} }
return !calc.isEmpty(); return !values.isEmpty();
} }
static vector<OBSThemeVariable> ParseThemeVariables(const char *themeData) static vector<OBSThemeVariable> ParseThemeVariables(const char *themeData)
@ -316,6 +324,11 @@ static vector<OBSThemeVariable> ParseThemeVariables(const char *themeData)
if (!cf_next_token(cfp)) if (!cf_next_token(cfp))
return vars; 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) { if (cfp->cur_token->type == CFTOKEN_NUM) {
const char *ch = cfp->cur_token->str.array; const char *ch = cfp->cur_token->str.array;
const char *end = ch + cfp->cur_token->str.len; const char *end = ch + cfp->cur_token->str.len;
@ -348,14 +361,20 @@ static vector<OBSThemeVariable> ParseThemeVariables(const char *themeData)
var.value = value; var.value = value;
var.type = OBSThemeVariable::Alias; var.type = OBSThemeVariable::Alias;
} else if (cf_token_is(cfp, "calc")) { } else if (cf_token_is(cfp, "calc") || cf_token_is(cfp, "max") || cf_token_is(cfp, "min")) {
QStringList calc; 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; continue;
var.type = OBSThemeVariable::Calc; var.value = values;
var.value = calc;
} else { } else {
var.type = OBSThemeVariable::String; var.type = OBSThemeVariable::String;
BPtr strVal = cf_literal_to_str(cfp->cur_token->str.array, cfp->cur_token->str.len); BPtr strVal = cf_literal_to_str(cfp->cur_token->str.array, cfp->cur_token->str.len);
@ -367,8 +386,9 @@ static vector<OBSThemeVariable> ParseThemeVariables(const char *themeData)
if (cf_token_is(cfp, "!") && if (cf_token_is(cfp, "!") &&
cf_next_token_should_be(cfp, "editable", nullptr, nullptr) == PARSE_SUCCESS) { cf_next_token_should_be(cfp, "editable", nullptr, nullptr) == PARSE_SUCCESS) {
if (var.type == OBSThemeVariable::Calc || var.type == OBSThemeVariable::Alias) { if (var.type == OBSThemeVariable::Calc || var.type == OBSThemeVariable::Max ||
blog(LOG_WARNING, "Variable of calc/alias type cannot be editable: %s", 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)); QT_TO_UTF8(var.name));
} else { } else {
var.editable = true; var.editable = true;
@ -496,10 +516,10 @@ static bool ResolveVariable(const QHash<QString, OBSThemeVariable> &vars, OBSThe
return true; return true;
} }
static QString EvalCalc(const QHash<QString, OBSThemeVariable> &vars, const OBSThemeVariable &var, static QString EvalMath(const QHash<QString, OBSThemeVariable> &vars, const OBSThemeVariable &var,
const int recursion = 0); const OBSThemeVariable::VariableType type, const int recursion = 0);
static OBSThemeVariable ParseCalcVariable(const QHash<QString, OBSThemeVariable> &vars, const QString &value, static OBSThemeVariable ParseMathVariable(const QHash<QString, OBSThemeVariable> &vars, const QString &value,
const int recursion = 0) const int recursion = 0)
{ {
OBSThemeVariable var; OBSThemeVariable var;
@ -527,15 +547,17 @@ static OBSThemeVariable ParseCalcVariable(const QHash<QString, OBSThemeVariable>
var.value = value; var.value = value;
ResolveVariable(vars, var); ResolveVariable(vars, var);
/* Handle nested calc()s */ /* Handle nested math calculations */
if (var.type == OBSThemeVariable::Calc) { if (var.type == OBSThemeVariable::Calc || var.type == OBSThemeVariable::Max ||
QString val = EvalCalc(vars, var, recursion + 1); var.type == OBSThemeVariable::Min) {
var = ParseCalcVariable(vars, val); QString val = EvalMath(vars, var, var.type, recursion + 1);
var = ParseMathVariable(vars, val);
} }
/* Only number or size would be valid here */ /* Only number or size would be valid here */
if (var.type != OBSThemeVariable::Number && var.type != OBSThemeVariable::Size) { 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"); throw invalid_argument("Operand not of numeric type");
} }
} }
@ -543,54 +565,67 @@ static OBSThemeVariable ParseCalcVariable(const QHash<QString, OBSThemeVariable>
return var; return var;
} }
static QString EvalCalc(const QHash<QString, OBSThemeVariable> &vars, const OBSThemeVariable &var, const int recursion) static QString EvalMath(const QHash<QString, OBSThemeVariable> &vars, const OBSThemeVariable &var,
const OBSThemeVariable::VariableType type, const int recursion)
{ {
if (recursion >= 10) { if (recursion >= 10) {
/* Abort after 10 levels of recursion */ /* Abort after 10 levels of recursion */
blog(LOG_ERROR, "Maximum calc() recursion levels hit!"); blog(LOG_ERROR, "Maximum recursion levels hit!");
return "'Invalid expression'";
}
if (type != OBSThemeVariable::Calc && type != OBSThemeVariable::Max && type != OBSThemeVariable::Min) {
blog(LOG_ERROR, "Invalid type for math operation!");
return "'Invalid expression'"; return "'Invalid expression'";
} }
QStringList args = var.value.toStringList(); QStringList args = var.value.toStringList();
if (args.length() != 3) { QString &opt = args[1];
blog(LOG_ERROR, "calc() had invalid number of arguments: %lld (%s)", args.length(), if (type == OBSThemeVariable::Calc && (opt != '*' && opt != '+' && opt != '-' && opt != '/')) {
QT_TO_UTF8(args.join(", "))); blog(LOG_ERROR, "Unknown/invalid calc() operator: %s", QT_TO_UTF8(opt));
return "'Invalid expression'"; return "'Invalid expression'";
} }
QString &opt = args[1]; if ((type == OBSThemeVariable::Max || type == OBSThemeVariable::Min) && opt != ',') {
if (opt != '*' && opt != '+' && opt != '-' && opt != '/') { blog(LOG_ERROR, "Invalid math separator: %s", QT_TO_UTF8(opt));
blog(LOG_ERROR, "Unknown/invalid calc() operator: %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'"; return "'Invalid expression'";
} }
OBSThemeVariable val1, val2; OBSThemeVariable val1, val2;
try { try {
val1 = ParseCalcVariable(vars, args[0], recursion); val1 = ParseMathVariable(vars, args[0], 0);
val2 = ParseCalcVariable(vars, args[2], recursion); val2 = ParseMathVariable(vars, args[2], 0);
} catch (...) { } catch (...) {
return "'Invalid expression'"; return "'Invalid expression'";
} }
/* Ensure that suffixes match (if any) */ /* Ensure that suffixes match (if any) */
if (!val1.suffix.isEmpty() && !val2.suffix.isEmpty() && val1.suffix != val2.suffix) { 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)); QT_TO_UTF8(val1.suffix), QT_TO_UTF8(val2.suffix));
return "'Invalid expression'"; return "'Invalid expression'";
} }
double val = numeric_limits<double>::quiet_NaN();
double d1 = val1.userValue.isValid() ? val1.userValue.toDouble() : val1.value.toDouble(); double d1 = val1.userValue.isValid() ? val1.userValue.toDouble() : val1.value.toDouble();
double d2 = val2.userValue.isValid() ? val2.userValue.toDouble() : val2.value.toDouble(); double d2 = val2.userValue.isValid() ? val2.userValue.toDouble() : val2.value.toDouble();
if (!isfinite(d1) || !isfinite(d2)) { if (!isfinite(d1) || !isfinite(d2)) {
blog(LOG_ERROR, blog(LOG_ERROR,
"calc() received at least one invalid value:" "At least one invalid math value:"
" op1: %f, op2: %f", " op1: %f, op2: %f",
d1, d2); d1, d2);
return "'Invalid expression'"; return "'Invalid expression'";
} }
double val = numeric_limits<double>::quiet_NaN();
if (type == OBSThemeVariable::Calc) {
if (opt == "+") if (opt == "+")
val = d1 + d2; val = d1 + d2;
else if (opt == "-") else if (opt == "-")
@ -601,12 +636,15 @@ static QString EvalCalc(const QHash<QString, OBSThemeVariable> &vars, const OBST
val = d1 / d2; val = d1 / d2;
if (!isnormal(val)) { if (!isnormal(val)) {
blog(LOG_ERROR, blog(LOG_ERROR, "Invalid calc() resulted in non-normal number: %f %s %f = %f", d1,
"Invalid calc() math resulted in non-normal number:" QT_TO_UTF8(opt), d2, val);
" %f %s %f = %f",
d1, QT_TO_UTF8(opt), d2, val);
return "'Invalid expression'"; 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; bool isInteger = ceill(val) == val;
QString result = QString::number(val, 'f', isInteger ? 0 : -1); QString result = QString::number(val, 'f', isInteger ? 0 : -1);
@ -661,8 +699,9 @@ static QString PrepareQSS(const QHash<QString, OBSThemeVariable> &vars, const QS
if (var.type == OBSThemeVariable::Color) { if (var.type == OBSThemeVariable::Color) {
replace = value.value<QColor>().name(QColor::HexRgb); replace = value.value<QColor>().name(QColor::HexRgb);
} else if (var.type == OBSThemeVariable::Calc) { } else if (var.type == OBSThemeVariable::Calc || var.type == OBSThemeVariable::Max ||
replace = EvalCalc(vars, var); var.type == OBSThemeVariable::Min) {
replace = EvalMath(vars, var, var.type);
} else if (var.type == OBSThemeVariable::Size || var.type == OBSThemeVariable::Number) { } else if (var.type == OBSThemeVariable::Size || var.type == OBSThemeVariable::Number) {
double val = value.toDouble(); double val = value.toDouble();
bool isInteger = ceill(val) == val; bool isInteger = ceill(val) == val;
@ -747,6 +786,23 @@ static QPalette PreparePalette(const QHash<QString, OBSThemeVariable> &vars, con
return pal; 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) OBSTheme *OBSApp::GetTheme(const QString &name)
{ {
if (!themes.contains(name)) if (!themes.contains(name))
@ -775,6 +831,22 @@ bool OBSApp::SetTheme(const QString &name)
QStringList themeIds(theme->dependencies); QStringList themeIds(theme->dependencies);
themeIds << theme->id; 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 */ /* Find and add high contrast adjustment layer if available */
if (HighContrastEnabled()) { if (HighContrastEnabled()) {
for (const OBSTheme &theme_ : themes) { for (const OBSTheme &theme_ : themes) {
@ -805,6 +877,22 @@ bool OBSApp::SetTheme(const QString &name)
contents.emplaceBack(content.constData()); 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 QString stylesheet = PrepareQSS(vars, contents);
const QPalette palette = PreparePalette(vars, defaultPalette); const QPalette palette = PreparePalette(vars, defaultPalette);
setPalette(palette); setPalette(palette);

View File

@ -1,16 +1,23 @@
#include "AbsoluteSlider.hpp" #include "AbsoluteSlider.hpp"
#include <QPainter>
#include "moc_AbsoluteSlider.cpp" #include "moc_AbsoluteSlider.cpp"
AbsoluteSlider::AbsoluteSlider(QWidget *parent) : SliderIgnoreScroll(parent) AbsoluteSlider::AbsoluteSlider(QWidget *parent) : SliderIgnoreScroll(parent)
{ {
installEventFilter(this); installEventFilter(this);
setMouseTracking(true); setMouseTracking(true);
tickColor.setRgb(0x5b, 0x62, 0x73);
} }
AbsoluteSlider::AbsoluteSlider(Qt::Orientation orientation, QWidget *parent) : SliderIgnoreScroll(orientation, parent) AbsoluteSlider::AbsoluteSlider(Qt::Orientation orientation, QWidget *parent) : SliderIgnoreScroll(orientation, parent)
{ {
installEventFilter(this); installEventFilter(this);
setMouseTracking(true); setMouseTracking(true);
tickColor.setRgb(0x5b, 0x62, 0x73);
} }
void AbsoluteSlider::mousePressEvent(QMouseEvent *event) void AbsoluteSlider::mousePressEvent(QMouseEvent *event)
@ -96,3 +103,64 @@ int AbsoluteSlider::posToRangeValue(QMouseEvent *event)
return sliderValue; 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);
}

View File

@ -4,11 +4,18 @@
class AbsoluteSlider : public SliderIgnoreScroll { class AbsoluteSlider : public SliderIgnoreScroll {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QColor tickColor READ getTickColor WRITE setTickColor DESIGNABLE true)
public: public:
AbsoluteSlider(QWidget *parent = nullptr); AbsoluteSlider(QWidget *parent = nullptr);
AbsoluteSlider(Qt::Orientation orientation, QWidget *parent = nullptr); AbsoluteSlider(Qt::Orientation orientation, QWidget *parent = nullptr);
bool getDisplayTicks() const;
void setDisplayTicks(bool display);
QColor getTickColor() const;
void setTickColor(QColor c);
signals: signals:
void absoluteSliderHovered(int value); void absoluteSliderHovered(int value);
@ -20,6 +27,11 @@ protected:
int posToRangeValue(QMouseEvent *event); int posToRangeValue(QMouseEvent *event);
virtual void paintEvent(QPaintEvent *event) override;
private: private:
bool dragging = false; bool dragging = false;
bool displayTicks = false;
QColor tickColor;
}; };

View File

@ -93,15 +93,9 @@
/* Layout */ /* Layout */
/* Configurable Values */ /* Configurable Values */
--font_base_value: var(--obsFontScale);
/* TODO: Min 8, Max 12, Step 1 */ --padding_base_value: var(--obsPadding);
--font_base_value: 10; --spacing_base_value: calc(2 + calc(var(--obsPadding) / 2));
/* TODO: Min 2, Max 7, Step 1 */
--spacing_base_value: 4;
/* TODO: Min 0.25, Max 10, Step 2 */
--padding_base_value: 4;
/* TODO: Better Accessibility focus state */ /* TODO: Better Accessibility focus state */
/* TODO: Move Accessibilty Colors to Theme config system */ /* TODO: Move Accessibilty Colors to Theme config system */
@ -111,26 +105,27 @@
--os_mac_font_base_value: 12; --os_mac_font_base_value: 12;
--font_base: calc(1pt * var(--font_base_value)); --font_base: calc(1pt * var(--font_base_value));
--font_small: calc(0.9pt * var(--font_base_value)); --font_small: max(7pt, calc(0.8pt * var(--font_base_value)));
--font_xsmall: calc(0.85pt * 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_large: calc(1.1pt * var(--font_base_value));
--font_xlarge: calc(1.5pt * var(--font_base_value)); --font_xlarge: calc(1.5pt * var(--font_base_value));
--font_heading: calc(2.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_base: min(max(1px, calc(0.4 * var(--spacing_base_value))), 2px);
--spacing_large: calc(1px * var(--spacing_base_value)); --spacing_large: min(max(2px, calc(1px * var(--spacing_base_value))), 4px);
--spacing_small: calc(0.25px * var(--spacing_base_value)); --spacing_small: max(1px, calc(0.25px * var(--spacing_base_value)));
--spacing_title: 4px; --spacing_title: 4px;
--padding_base: calc(0.5px * var(--padding_base_value)); --padding_base: calc(0.5px * var(--padding_base_value));
--padding_large: calc(1px * var(--padding_base_value)); --padding_large: min(max(1px, calc(1px * var(--padding_base_value))), 5px);
--padding_xlarge: calc(1.75px * var(--padding_base_value)); --padding_xlarge: min(max(2px, calc(1.75px * var(--padding_base_value))), 10px);
--padding_small: calc(0.25px * var(--padding_base_value)); --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_menu: calc(4px + calc(2 * var(--padding_base_value)));
--padding_base_border: calc(var(--padding_base) + 1px); --padding_base_border: calc(var(--padding_base) + 1px);
@ -154,9 +149,10 @@
--input_font_scale: calc(var(--font_base_value) * 2.2); --input_font_scale: calc(var(--font_base_value) * 2.2);
--input_font_padding: calc(var(--padding_base_value) * 2); --input_font_padding: calc(var(--padding_base_value) * 2);
--input_height_base: calc(var(--input_font_scale) + var(--input_font_padding)); --input_height_base: max(calc(var(--input_font_scale) + var(--input_font_padding)), 24);
--input_padding: var(--padding_large); --input_padding: calc(2px + var(--padding_base));
--input_height: calc(var(--input_height_base) - calc(var(--input_padding) * 2)); --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_height_half: calc(var(--input_height_base) / 2);
--input_bg: var(--grey4); --input_bg: var(--grey4);
@ -196,6 +192,8 @@
--scrollbar_down: var(--grey8); --scrollbar_down: var(--grey8);
--scrollbar_border: var(--grey2); --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); --separator_hover: var(--white1);
--highlight: rgb(42, 130, 218); --highlight: rgb(42, 130, 218);
@ -449,6 +447,9 @@ QListWidget QWidget {
border: 1px solid var(--bg_base); border: 1px solid var(--bg_base);
} }
* {
spacing: var(--spacing_small);
}
/* Misc */ /* Misc */
@ -612,7 +613,7 @@ OBSDock > QWidget {
} }
#transitionsFrame { #transitionsFrame {
padding: var(--padding_large); padding: var(--padding_container);
} }
OBSDock QLabel { OBSDock QLabel {
@ -661,16 +662,15 @@ QScrollArea {
* oversize it and use margin to crunch it back down * oversize it and use margin to crunch it back down
*/ */
OBSBasicStatusBar { OBSBasicStatusBar {
margin-top: 4px; margin-top: var(--spacing_large);
border-top: 1px solid var(--border_color); border-top: 1px solid var(--border_color);
background: var(--bg_base); background: var(--bg_base);
} }
StatusBarWidget > QFrame { StatusBarWidget > QFrame {
margin-top: 1px;
border: 0px solid var(--border_color); border: 0px solid var(--border_color);
border-left-width: 1px; border-left-width: 1px;
padding: 0px 8px 2px; padding: 0px var(--padding_xlarge) var(--padding_small);
} }
/* Group Box */ /* Group Box */
@ -803,6 +803,7 @@ QToolBar {
background-color: transparent; background-color: transparent;
border: none; border: none;
margin: var(--spacing_base) 0px; margin: var(--spacing_base) 0px;
spacing: var(--spacing_base);
} }
QToolBarExtension { QToolBarExtension {
@ -893,11 +894,10 @@ QTabBar QToolButton {
QComboBox, QComboBox,
QDateTimeEdit { QDateTimeEdit {
background-color: var(--input_bg); background-color: var(--input_bg);
border-style: solid;
border: 1px solid var(--input_bg); border: 1px solid var(--input_bg);
border-radius: var(--border_radius); border-radius: var(--border_radius);
padding: var(--padding_large) var(--padding_large); padding: var(--input_padding) var(--input_text_padding);
padding-left: 10px; height: var(--input_height);
} }
QComboBox QAbstractItemView { QComboBox QAbstractItemView {
@ -974,8 +974,7 @@ QPlainTextEdit {
background-color: var(--input_bg); background-color: var(--input_bg);
border: none; border: none;
border-radius: var(--border_radius); border-radius: var(--border_radius);
padding: var(--input_padding) var(--padding_small) var(--input_padding) var(--input_padding); padding: var(--input_padding) var(--input_text_padding);
padding-left: 8px;
border: 1px solid var(--input_bg); border: 1px solid var(--input_bg);
height: var(--input_height); height: var(--input_height);
} }
@ -994,6 +993,13 @@ QPlainTextEdit:focus {
border-color: var(--input_border_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,
QTextEdit:!editable:hover, QTextEdit:!editable:hover,
QTextEdit:!editable:focus { QTextEdit:!editable:focus {
@ -1007,8 +1013,8 @@ QDoubleSpinBox {
background-color: var(--input_bg); background-color: var(--input_bg);
border: 1px solid var(--input_bg); border: 1px solid var(--input_bg);
border-radius: var(--border_radius); border-radius: var(--border_radius);
padding: var(--input_padding) 0px var(--input_padding) var(--input_padding); padding: var(--input_padding) var(--input_text_padding);
padding-left: 8px; height: var(--input_height);
max-height: var(--input_height); max-height: var(--input_height);
} }
@ -1096,7 +1102,7 @@ QDoubleSpinBox::down-arrow {
/* Controls Dock */ /* Controls Dock */
#controlsFrame { #controlsFrame {
padding: var(--padding_large); padding: var(--padding_container);
} }
#controlsFrame QPushButton { #controlsFrame QPushButton {
@ -1143,55 +1149,26 @@ QDoubleSpinBox::down-arrow {
/* Buttons */ /* Buttons */
QPushButton { QPushButton {
color: var(--text);
background-color: var(--button_bg); background-color: var(--button_bg);
color: var(--text);
border: 1px solid var(--button_border);
border-radius: var(--border_radius); border-radius: var(--border_radius);
height: var(--input_height); height: var(--input_height);
max-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); padding: var(--input_padding) var(--padding_wide);
icon-size: var(--icon_base); icon-size: var(--icon_base);
} outline: none;
QPushButton {
border: 1px solid var(--button_border);
}
QToolButton {
border: 1px solid var(--button_border);
}
QToolButton,
.btn-tool {
background-color: var(--button_bg);
padding: var(--padding_base) var(--padding_base);
margin: 0px var(--spacing_base);
border: 1px solid var(--button_border);
border-radius: var(--border_radius);
icon-size: var(--icon_base);
}
QToolButton:last-child,
.btn-tool:last-child {
margin-right: 0px;
}
QPushButton:hover,
QPushButton:focus {
border-color: var(--button_border_hover);
} }
QPushButton:hover { QPushButton:hover {
background-color: var(--button_bg_hover); background-color: var(--button_bg_hover);
} }
QToolButton:hover, QPushButton:hover,
QToolButton:focus, QPushButton:focus {
.btn-tool:hover, border-color: var(--button_border_hover);
.btn-tool:focus,
.indicator-mute::indicator:hover,
.indicator-mute::indicator:focus {
border-color: var(--button_border);
background-color: var(--button_bg_hover);
} }
QPushButton::flat { QPushButton::flat {
@ -1200,6 +1177,7 @@ QPushButton::flat {
QPushButton:checked { QPushButton:checked {
background-color: var(--primary); background-color: var(--primary);
border-color: var(--primary_light);
} }
QPushButton:checked:hover, QPushButton:checked:hover,
@ -1213,6 +1191,47 @@ QPushButton:pressed:hover {
border-color: var(--button_border); 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 {
border: 1px solid var(--button_border);
}
QToolButton,
.btn-tool {
background-color: var(--button_bg);
padding: var(--padding_base) var(--padding_base);
margin: 0px 0px;
border: 1px solid var(--button_border);
border-radius: var(--border_radius);
icon-size: var(--icon_base);
}
QToolButton:last-child,
.btn-tool:last-child {
margin-right: 0px;
}
QToolButton:hover,
QToolButton:focus,
.btn-tool:hover,
.btn-tool:focus,
.indicator-mute::indicator:hover,
.indicator-mute::indicator:focus {
border-color: var(--button_border);
background-color: var(--button_bg_hover);
}
QToolButton:pressed, QToolButton:pressed,
QToolButton:pressed:hover, QToolButton:pressed:hover,
.btn-tool:pressed, .btn-tool:pressed,
@ -1221,24 +1240,12 @@ QToolButton:pressed:hover,
border-color: var(--button_border); border-color: var(--button_border);
} }
QPushButton:disabled {
background-color: var(--button_bg_disabled);
border-color: var(--button_border);
}
QToolButton:disabled, QToolButton:disabled,
.btn-tool:disabled { .btn-tool:disabled {
background-color: var(--button_bg_disabled); background-color: var(--button_bg_disabled);
border-color: transparent; border-color: transparent;
} }
QPushButton::menu-indicator {
image: url(theme:Dark/down.svg);
subcontrol-position: right;
subcontrol-origin: padding;
width: 25px;
}
/* Sliders */ /* Sliders */
QSlider::groove { QSlider::groove {
@ -1309,7 +1316,7 @@ QSlider::handle:hover {
} }
QSlider::handle:pressed { QSlider::handle:pressed {
background-color: var(--white5); background-color: var(--white3);
} }
QSlider::handle:disabled { QSlider::handle:disabled {
@ -1349,6 +1356,15 @@ QSlider::handle:disabled {
border-bottom: 1px solid #3c404b; border-bottom: 1px solid #3c404b;
} }
VolControl {
background: var(--bg_base);
}
VolControl QLabel {
font-size: var(--font_small);
margin: var(--spacing_small) 0px;
}
VolControl #volLabel { VolControl #volLabel {
padding: var(--padding_base) 0px var(--padding_base); padding: var(--padding_base) 0px var(--padding_base);
text-align: center; text-align: center;
@ -1377,7 +1393,7 @@ VolControl #volLabel {
} }
#vMixerScrollArea VolControl { #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); border-right: 1px solid var(--border_color);
} }
@ -1407,6 +1423,7 @@ VolControl #volLabel {
} }
#vMixerScrollArea VolControl QPushButton { #vMixerScrollArea VolControl QPushButton {
margin-left: var(--spacing_base);
margin-right: var(--padding_xlarge); margin-right: var(--padding_xlarge);
} }
@ -1414,10 +1431,6 @@ VolControl #volLabel {
margin-left: var(--padding_xlarge); margin-left: var(--padding_xlarge);
} }
VolControl {
background: var(--bg_base);
}
VolumeMeter { VolumeMeter {
background: transparent; background: transparent;
} }
@ -1527,6 +1540,7 @@ QGroupBox::indicator,
QTableView::indicator { QTableView::indicator {
width: var(--icon_base); width: var(--icon_base);
height: var(--icon_base); height: var(--icon_base);
margin-right: var(--spacing_large);
} }
QGroupBox::indicator { QGroupBox::indicator {
@ -1952,7 +1966,7 @@ OBSBasicAdvAudio #scrollAreaWidgetContents {
font-size: var(--font_xsmall); font-size: var(--font_xsmall);
height: 14px; height: 14px;
max-height: 14px; max-height: 14px;
padding: 0px var(--padding_xlarge); padding: 0px;
margin: 0; margin: 0;
border: none; border: none;
border-radius: 0; border-radius: 0;
@ -1962,7 +1976,13 @@ OBSBasicAdvAudio #scrollAreaWidgetContents {
border: 1px solid var(--grey6); border: 1px solid var(--grey6);
} }
#previewScalePercent {
padding: 0px var(--input_text_padding);
min-width: var(--preview_scale_width);
}
#previewScalingMode { #previewScalingMode {
padding: 0px var(--input_text_padding);
border: 1px solid var(--grey6); border: 1px solid var(--grey6);
} }

View File

@ -24,23 +24,13 @@
--primary_light: rgb(33,71,109); --primary_light: rgb(33,71,109);
/* Layout */ /* Layout */
--font_base_value: 9; --font_small: max(7pt, calc(0.5pt * var(--font_base_value)));
--spacing_base_value: 2;
--padding_base_value: 0.25;
/* OS Fixes */ --padding_large: min(max(0px, calc(1px * var(--padding_base_value))), 5px);
--os_mac_font_base_value: 11;
--font_small: calc(0.75pt * var(--font_base_value)); --padding_container: max(2px, var(--padding_base));
--icon_base: calc(6px + var(--font_base_value)); /* Inputs / Controls */
--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)));
--border_color: var(--grey6); --border_color: var(--grey6);
@ -48,6 +38,10 @@
--border_radius_small: 1px; --border_radius_small: 1px;
--border_radius_large: 2px; --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: var(--grey4);
--input_bg_hover: var(--grey1); --input_bg_hover: var(--grey1);
--input_bg_focus: var(--grey6); --input_bg_focus: var(--grey6);
@ -263,7 +257,6 @@ QPushButton[toolButton="true"] {
#vMixerScrollArea QLabel { #vMixerScrollArea QLabel {
font-size: var(--font_small); font-size: var(--font_small);
margin: var(--padding_xlarge) 0px;
} }
#vMixerScrollArea #volLabel { #vMixerScrollArea #volLabel {

View File

@ -979,7 +979,7 @@
<item> <item>
<widget class="QGroupBox" name="appearanceGeneral"> <widget class="QGroupBox" name="appearanceGeneral">
<property name="title"> <property name="title">
<string>Basic.Settings.Appearance.General</string> <string>Basic.Settings.Appearance</string>
</property> </property>
<property name="checkable"> <property name="checkable">
<bool>false</bool> <bool>false</bool>
@ -1021,6 +1021,200 @@
<widget class="QComboBox" name="themeVariant"/> <widget class="QComboBox" name="themeVariant"/>
</item> </item>
<item row="2" column="0"> <item row="2" column="0">
<widget class="QLabel" name="appearanceSettingLabelFontScale">
<property name="text">
<string>Font Size</string>
</property>
<property name="buddy">
<cstring>appearanceFontScale</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QFrame" name="frame_2">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_23" stretch="0,5">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="appearanceFontScaleText">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="text">
<string>10</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="AbsoluteSlider" name="appearanceFontScale">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimum">
<number>8</number>
</property>
<property name="maximum">
<number>12</number>
</property>
<property name="pageStep">
<number>2</number>
</property>
<property name="value">
<number>10</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBothSides</enum>
</property>
<property name="tickInterval">
<number>1</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_20">
<property name="text">
<string>Density</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QFrame" name="frame_5">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_34">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="appearanceDensity1">
<property name="text">
<string>Classic</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="autoExclusive">
<bool>true</bool>
</property>
<attribute name="buttonGroup">
<string notr="true">appearanceDensityButtonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QPushButton" name="appearanceDensity2">
<property name="text">
<string>Compact</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="autoExclusive">
<bool>true</bool>
</property>
<attribute name="buttonGroup">
<string notr="true">appearanceDensityButtonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QPushButton" name="appearanceDensity3">
<property name="text">
<string>Normal</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="autoExclusive">
<bool>true</bool>
</property>
<attribute name="buttonGroup">
<string notr="true">appearanceDensityButtonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QPushButton" name="appearanceDensity4">
<property name="text">
<string>Comfortable</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="autoExclusive">
<bool>true</bool>
</property>
<attribute name="buttonGroup">
<string notr="true">appearanceDensityButtonGroup</string>
</attribute>
</widget>
</item>
</layout>
</widget>
</item>
<item row="4" column="0">
<spacer name="horizontalSpacer_17"> <spacer name="horizontalSpacer_17">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Horizontal</enum>
@ -1028,7 +1222,7 @@
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>170</width> <width>170</width>
<height>0</height> <height>10</height>
</size> </size>
</property> </property>
</spacer> </spacer>
@ -1049,6 +1243,31 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item>
<widget class="QFrame" name="appearanceOptionsWarning">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QVBoxLayout" name="verticalLayout_35">
<item>
<widget class="QLabel" name="appearanceOptionsWarningLabel">
<property name="text">
<string>Some appearance options are not available for this style.</string>
</property>
<property name="class" stdset="0">
<string>text-warning</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -2898,8 +3117,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>766</width> <width>424</width>
<height>592</height> <height>175</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_14"> <layout class="QVBoxLayout" name="verticalLayout_14">
@ -3304,8 +3523,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>766</width> <width>509</width>
<height>558</height> <height>371</height>
</rect> </rect>
</property> </property>
<property name="sizePolicy"> <property name="sizePolicy">
@ -3945,8 +4164,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>766</width> <width>625</width>
<height>558</height> <height>489</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_27"> <layout class="QVBoxLayout" name="verticalLayout_27">
@ -4495,8 +4714,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>766</width> <width>258</width>
<height>592</height> <height>510</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_28"> <layout class="QVBoxLayout" name="verticalLayout_28">
@ -8479,6 +8698,11 @@
<extends>QLineEdit</extends> <extends>QLineEdit</extends>
<header>settings/OBSHotkeyEdit.hpp</header> <header>settings/OBSHotkeyEdit.hpp</header>
</customwidget> </customwidget>
<customwidget>
<class>AbsoluteSlider</class>
<extends>QSlider</extends>
<header>components/AbsoluteSlider.hpp</header>
</customwidget>
</customwidgets> </customwidgets>
<tabstops> <tabstops>
<tabstop>listWidget</tabstop> <tabstop>listWidget</tabstop>
@ -9058,4 +9282,7 @@
</hints> </hints>
</connection> </connection>
</connections> </connections>
<buttongroups>
<buttongroup name="appearanceDensityButtonGroup"/>
</buttongroups>
</ui> </ui>

View File

@ -297,6 +297,7 @@ void RestrictResetBitrates(initializer_list<QComboBox *> boxes, int maxbitrate);
#define SCROLL_CHANGED &QSpinBox::valueChanged #define SCROLL_CHANGED &QSpinBox::valueChanged
#define DSCROLL_CHANGED &QDoubleSpinBox::valueChanged #define DSCROLL_CHANGED &QDoubleSpinBox::valueChanged
#define TEXT_CHANGED &QPlainTextEdit::textChanged #define TEXT_CHANGED &QPlainTextEdit::textChanged
#define SLIDER_CHANGED &QSlider::valueChanged
#define GENERAL_CHANGED &OBSBasicSettings::GeneralChanged #define GENERAL_CHANGED &OBSBasicSettings::GeneralChanged
#define STREAM1_CHANGED &OBSBasicSettings::Stream1Changed #define STREAM1_CHANGED &OBSBasicSettings::Stream1Changed
@ -368,6 +369,11 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
HookWidget(ui->multiviewLayout, COMBO_CHANGED, GENERAL_CHANGED); HookWidget(ui->multiviewLayout, COMBO_CHANGED, GENERAL_CHANGED);
HookWidget(ui->theme, COMBO_CHANGED, APPEAR_CHANGED); HookWidget(ui->theme, COMBO_CHANGED, APPEAR_CHANGED);
HookWidget(ui->themeVariant, 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->service, COMBO_CHANGED, STREAM1_CHANGED);
HookWidget(ui->server, COMBO_CHANGED, STREAM1_CHANGED); HookWidget(ui->server, COMBO_CHANGED, STREAM1_CHANGED);
HookWidget(ui->customServer, EDIT_CHANGED, STREAM1_CHANGED); HookWidget(ui->customServer, EDIT_CHANGED, STREAM1_CHANGED);

View File

@ -225,6 +225,8 @@ private:
/* Appearance */ /* Appearance */
void InitAppearancePage(); void InitAppearancePage();
void enableAppearanceFontControls(bool enable);
void enableAppearanceDensityControls(bool enable);
bool IsCustomServer(); bool IsCustomServer();
@ -346,6 +348,7 @@ private:
private slots: private slots:
void on_theme_activated(int idx); void on_theme_activated(int idx);
void on_themeVariant_activated(int idx); void on_themeVariant_activated(int idx);
void updateAppearanceControls();
void on_listWidget_itemSelectionChanged(); void on_listWidget_itemSelectionChanged();
void on_buttonBox_clicked(QAbstractButton *button); void on_buttonBox_clicked(QAbstractButton *button);

View File

@ -21,6 +21,15 @@ void OBSBasicSettings::InitAppearancePage()
ui->theme->setCurrentIndex(idx); ui->theme->setCurrentIndex(idx);
ui->themeVariant->setPlaceholderText(QTStr("Basic.Settings.Appearance.General.NoVariant")); 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) void OBSBasicSettings::LoadThemeList(bool reload)
@ -83,6 +92,16 @@ void OBSBasicSettings::LoadAppearanceSettings(bool reload)
App()->SetTheme(themeId); 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() void OBSBasicSettings::SaveAppearanceSettings()
@ -93,6 +112,13 @@ void OBSBasicSettings::SaveAppearanceSettings()
if (savedTheme != currentTheme) { if (savedTheme != currentTheme) {
config_set_string(config, "Appearance", "Theme", QT_TO_UTF8(currentTheme->id)); 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) void OBSBasicSettings::on_theme_activated(int)
@ -104,3 +130,30 @@ void OBSBasicSettings::on_themeVariant_activated(int)
{ {
LoadAppearanceSettings(true); 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)
{
const QList<QAbstractButton *> buttons = ui->appearanceDensityButtonGroup->buttons();
for (QAbstractButton *button : buttons) {
button->setEnabled(enable);
}
}

View File

@ -41,4 +41,7 @@ struct OBSTheme {
bool isVisible; /* Whether it should be shown to the user */ bool isVisible; /* Whether it should be shown to the user */
bool isBaseTheme; /* Whether it is a "style" or variant */ bool isBaseTheme; /* Whether it is a "style" or variant */
bool isHighContrast; /* Whether it is a high-contrast adjustment layer */ 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 */
}; };

View File

@ -28,6 +28,8 @@ struct OBSThemeVariable {
String, /* Raw string (e.g. color name, border style, etc.) */ String, /* Raw string (e.g. color name, border style, etc.) */
Alias, /* Points at another variable, value will be the key */ Alias, /* Points at another variable, value will be the key */
Calc, /* Simple calculation with two operands */ 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 */ /* Whether the variable should be editable in the UI */