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", "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)

View File

@ -204,7 +204,7 @@ static QColor ParseColor(CFParser &cfp)
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);
if (ret != PARSE_SUCCESS)
@ -216,36 +216,44 @@ static bool ParseCalc(CFParser &cfp, QStringList &calc, vector<OBSThemeVariable>
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<OBSThemeVariable> ParseThemeVariables(const char *themeData)
@ -316,6 +324,11 @@ static vector<OBSThemeVariable> 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<OBSThemeVariable> 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<OBSThemeVariable> 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<QString, OBSThemeVariable> &vars, OBSThe
return true;
}
static QString EvalCalc(const QHash<QString, OBSThemeVariable> &vars, const OBSThemeVariable &var,
const int recursion = 0);
static QString EvalMath(const QHash<QString, OBSThemeVariable> &vars, const OBSThemeVariable &var,
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)
{
OBSThemeVariable var;
@ -527,15 +547,17 @@ static OBSThemeVariable ParseCalcVariable(const QHash<QString, OBSThemeVariable>
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,85 @@ static OBSThemeVariable ParseCalcVariable(const QHash<QString, OBSThemeVariable>
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) {
/* 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'";
}
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(", ")));
QString &opt = args[1];
if (type == OBSThemeVariable::Calc && (opt != '*' && opt != '+' && opt != '-' && opt != '/')) {
blog(LOG_ERROR, "Unknown/invalid calc() operator: %s", QT_TO_UTF8(opt));
return "'Invalid expression'";
}
QString &opt = args[1];
if (opt != '*' && opt != '+' && opt != '-' && opt != '/') {
blog(LOG_ERROR, "Unknown/invalid calc() operator: %s", QT_TO_UTF8(opt));
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<double>::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<double>::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 +699,9 @@ static QString PrepareQSS(const QHash<QString, OBSThemeVariable> &vars, const QS
if (var.type == OBSThemeVariable::Color) {
replace = value.value<QColor>().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 +786,23 @@ static QPalette PreparePalette(const QHash<QString, OBSThemeVariable> &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 +831,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 +877,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);

View File

@ -1,16 +1,23 @@
#include "AbsoluteSlider.hpp"
#include <QPainter>
#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);
}

View File

@ -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);
bool getDisplayTicks() const;
void setDisplayTicks(bool display);
QColor getTickColor() const;
void setTickColor(QColor c);
signals:
void absoluteSliderHovered(int value);
@ -20,6 +27,11 @@ protected:
int posToRangeValue(QMouseEvent *event);
virtual void paintEvent(QPaintEvent *event) override;
private:
bool dragging = false;
bool displayTicks = false;
QColor tickColor;
};

View File

@ -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);
@ -449,6 +447,9 @@ QListWidget QWidget {
border: 1px solid var(--bg_base);
}
* {
spacing: var(--spacing_small);
}
/* Misc */
@ -612,7 +613,7 @@ OBSDock > QWidget {
}
#transitionsFrame {
padding: var(--padding_large);
padding: var(--padding_container);
}
OBSDock QLabel {
@ -661,16 +662,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 */
@ -803,6 +803,7 @@ QToolBar {
background-color: transparent;
border: none;
margin: var(--spacing_base) 0px;
spacing: var(--spacing_base);
}
QToolBarExtension {
@ -893,11 +894,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 {
@ -974,8 +974,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);
}
@ -994,6 +993,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 {
@ -1007,8 +1013,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);
}
@ -1096,7 +1102,7 @@ QDoubleSpinBox::down-arrow {
/* Controls Dock */
#controlsFrame {
padding: var(--padding_large);
padding: var(--padding_container);
}
#controlsFrame QPushButton {
@ -1143,55 +1149,26 @@ 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);
}
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);
outline: none;
}
QPushButton:hover {
background-color: var(--button_bg_hover);
}
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);
QPushButton:hover,
QPushButton:focus {
border-color: var(--button_border_hover);
}
QPushButton::flat {
@ -1200,6 +1177,7 @@ QPushButton::flat {
QPushButton:checked {
background-color: var(--primary);
border-color: var(--primary_light);
}
QPushButton:checked:hover,
@ -1213,6 +1191,47 @@ QPushButton:pressed:hover {
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:hover,
.btn-tool:pressed,
@ -1221,24 +1240,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 {
@ -1309,7 +1316,7 @@ QSlider::handle:hover {
}
QSlider::handle:pressed {
background-color: var(--white5);
background-color: var(--white3);
}
QSlider::handle:disabled {
@ -1349,6 +1356,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;
@ -1377,7 +1393,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);
}
@ -1407,6 +1423,7 @@ VolControl #volLabel {
}
#vMixerScrollArea VolControl QPushButton {
margin-left: var(--spacing_base);
margin-right: var(--padding_xlarge);
}
@ -1414,10 +1431,6 @@ VolControl #volLabel {
margin-left: var(--padding_xlarge);
}
VolControl {
background: var(--bg_base);
}
VolumeMeter {
background: transparent;
}
@ -1527,6 +1540,7 @@ QGroupBox::indicator,
QTableView::indicator {
width: var(--icon_base);
height: var(--icon_base);
margin-right: var(--spacing_large);
}
QGroupBox::indicator {
@ -1952,7 +1966,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;
@ -1962,7 +1976,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);
}

View File

@ -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 {

View File

@ -979,7 +979,7 @@
<item>
<widget class="QGroupBox" name="appearanceGeneral">
<property name="title">
<string>Basic.Settings.Appearance.General</string>
<string>Basic.Settings.Appearance</string>
</property>
<property name="checkable">
<bool>false</bool>
@ -1021,6 +1021,200 @@
<widget class="QComboBox" name="themeVariant"/>
</item>
<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">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@ -1028,7 +1222,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>170</width>
<height>0</height>
<height>10</height>
</size>
</property>
</spacer>
@ -1049,6 +1243,31 @@
</property>
</spacer>
</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>
</widget>
</item>
@ -2898,8 +3117,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>766</width>
<height>592</height>
<width>424</width>
<height>175</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_14">
@ -3304,8 +3523,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>766</width>
<height>558</height>
<width>509</width>
<height>371</height>
</rect>
</property>
<property name="sizePolicy">
@ -3945,8 +4164,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>766</width>
<height>558</height>
<width>625</width>
<height>489</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_27">
@ -4495,8 +4714,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>766</width>
<height>592</height>
<width>258</width>
<height>510</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_28">
@ -8479,6 +8698,11 @@
<extends>QLineEdit</extends>
<header>settings/OBSHotkeyEdit.hpp</header>
</customwidget>
<customwidget>
<class>AbsoluteSlider</class>
<extends>QSlider</extends>
<header>components/AbsoluteSlider.hpp</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>listWidget</tabstop>
@ -9058,4 +9282,7 @@
</hints>
</connection>
</connections>
<buttongroups>
<buttongroup name="appearanceDensityButtonGroup"/>
</buttongroups>
</ui>

View File

@ -297,6 +297,7 @@ void RestrictResetBitrates(initializer_list<QComboBox *> 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);

View File

@ -225,6 +225,8 @@ private:
/* Appearance */
void InitAppearancePage();
void enableAppearanceFontControls(bool enable);
void enableAppearanceDensityControls(bool enable);
bool IsCustomServer();
@ -346,6 +348,7 @@ private:
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);

View File

@ -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)
{
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 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 */
};

View File

@ -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 */