mirror of
https://github.com/Mudlet/Mudlet
synced 2026-08-13 18:26:27 -04:00
add: full-window background image/gradient support (#9394)
#### Brief overview of PR changes/additions
- Updates `setBackgroundImage(target, imageLocation, [mode],
[fullWindow])` / `resetBackgroundImage(target, [fullWindow])` Lua
functions to support painting a background image or gradient behind the
entire main console window, independent of `main`'s own size/position.
- adds a new `cover` mode for true aspect-preserving scale-to-fill;
gradients are supported via the existing `style` mode using Qt's native
QSS gradient syntax
(`qlineargradient`/`qradialgradient`/`qconicalgradient`).
- Unlike previous `setBackgroundImage("main", ...)`, the background
stays fixed and uncropped when `main` is resized via `setBorderSizes()`
or side panels are added.
#### Motivation for adding to Mudlet
Lets players and package authors theme the whole Mudlet window with a
persistent image or gradient, without hacky miniconsole/image-slicing
workarounds that break whenever borders or panel layout change.
#### Other info (issues closed, discussion etc)
Closes #9392
**Test case:**
`setBorderSizes(80,80,80,80)` then
`setBackgroundImage("main", getMudletHomeDir().."/pathto/asset.png",
"cover", true)`
Image fills the border margin around `main`, uncropped, and stays fixed
through further `setBorderSizes()` changes. Combine with
`setBackgroundColor("main", 0,0,0,140)` for a tinted readable box.
`resetBackgroundImage("main", true)` removes it.
`setBackgroundImage("main","background: qlineargradient(x1:0, y1:0,
x2:0, y2:1, stop:0 #1a1a2e, stop:1 #16213e);", "style", true)` paints a
gradient behind "main"
`resetBackgroundImage("main", true)` removes it.
`setBackgroundImage("main", getMudletHomeDir().."/bgtest-assets/bg.png",
"border")` - Continues to add image as before
Example:
<img width="1482" height="846" alt="Screenshot 2026-07-08 at 2 59 17 PM"
src="https://github.com/user-attachments/assets/e2ec9835-a5c3-4f1a-a246-c45de6f80062"
/>
This commit is contained in:
parent
f9a785d402
commit
ae6b017c81
6 changed files with 195 additions and 34 deletions
24
src/Host.cpp
24
src/Host.cpp
|
|
@ -4541,16 +4541,24 @@ std::optional<QColor> Host::getBackgroundColor(const QString& name) const
|
|||
return {};
|
||||
}
|
||||
|
||||
bool Host::setBackgroundImage(const QString& name, QString& imgPath, int mode)
|
||||
bool Host::setBackgroundImage(const QString& name, QString& imgPath, int mode, bool fullWindow)
|
||||
{
|
||||
if (!mpConsole) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (QDir::homePath().contains('\\')) {
|
||||
imgPath.replace('/', R"(\)");
|
||||
} else {
|
||||
imgPath.replace('\\', "/");
|
||||
// Mode 4 ('style') is a raw QSS fragment (e.g. a gradient), not a literal file
|
||||
// path - normalising separators would corrupt any url()/gradient syntax in it:
|
||||
if (mode != 4) {
|
||||
if (QDir::homePath().contains('\\')) {
|
||||
imgPath.replace('/', R"(\)");
|
||||
} else {
|
||||
imgPath.replace('\\', "/");
|
||||
}
|
||||
}
|
||||
|
||||
if (fullWindow) {
|
||||
return mpConsole->setWindowBackgroundImage(imgPath, mode);
|
||||
}
|
||||
|
||||
if (name.isEmpty() || name.compare(qsl("main"), Qt::CaseSensitive) == 0) {
|
||||
|
|
@ -4573,12 +4581,16 @@ bool Host::setBackgroundImage(const QString& name, QString& imgPath, int mode)
|
|||
return false;
|
||||
}
|
||||
|
||||
bool Host::resetBackgroundImage(const QString& name)
|
||||
bool Host::resetBackgroundImage(const QString& name, bool fullWindow)
|
||||
{
|
||||
if (!mpConsole) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fullWindow) {
|
||||
return mpConsole->resetWindowBackgroundImage();
|
||||
}
|
||||
|
||||
if (name.isEmpty() || name.compare(qsl("main"), Qt::CaseSensitive) == 0) {
|
||||
mpConsole->resetConsoleBackgroundImage();
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -449,8 +449,8 @@ public:
|
|||
bool setCommandBackgroundColor(const QString& name, int r, int g, int b, int alpha);
|
||||
bool setCommandForegroundColor(const QString& name, int r, int g, int b, int alpha);
|
||||
std::optional<QColor> getBackgroundColor(const QString& name) const;
|
||||
bool setBackgroundImage(const QString& name, QString& path, int mode);
|
||||
bool resetBackgroundImage(const QString& name);
|
||||
bool setBackgroundImage(const QString& name, QString& path, int mode, bool fullWindow = false);
|
||||
bool resetBackgroundImage(const QString& name, bool fullWindow = false);
|
||||
void showHideOrCreateMapper(const bool loadDefaultMap);
|
||||
bool setProfileStyleSheet(const QString& styleSheet);
|
||||
void check_for_mappingscript();
|
||||
|
|
|
|||
143
src/TConsole.cpp
143
src/TConsole.cpp
|
|
@ -223,6 +223,19 @@ TConsole::TConsole(Host* pH, const QString& name, const ConsoleType type, QWidge
|
|||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(0);
|
||||
|
||||
if (mType == MainConsole) {
|
||||
// Sits behind mpMainDisplay within mpMainFrame, which always spans the
|
||||
// console's full pane regardless of mBorders - so this stays independent
|
||||
// of setBorderSizes() without needing its own resize/border logic.
|
||||
mpWindowBackground = new QWidget(mpMainFrame);
|
||||
mpWindowBackground->setObjectName(qsl("WindowBackground"));
|
||||
mpWindowBackground->setContentsMargins(0, 0, 0, 0);
|
||||
mpWindowBackground->move(0, 0);
|
||||
mpWindowBackground->resize(mpMainFrame->size());
|
||||
mpWindowBackground->lower();
|
||||
mpWindowBackground->show();
|
||||
}
|
||||
|
||||
mpBaseVFrame->setSizePolicy(sizePolicy);
|
||||
mpBaseHFrame->setSizePolicy(sizePolicy);
|
||||
|
||||
|
|
@ -716,6 +729,13 @@ void TConsole::resizeEvent(QResizeEvent* event)
|
|||
mpMainDisplay->resize(x, y);
|
||||
}
|
||||
|
||||
if (mpWindowBackground) {
|
||||
mpWindowBackground->resize(mpMainFrame->size());
|
||||
if (mWindowBgImageMode == 5) {
|
||||
updateWindowBackgroundCoverPixmap();
|
||||
}
|
||||
}
|
||||
|
||||
if (mType & (CentralDebugConsole | ErrorConsole)) {
|
||||
layerCommandLine->hide();
|
||||
} else if (mType & ~(SubConsole | UserWindow)) {
|
||||
|
|
@ -1008,6 +1028,24 @@ QString getColorCode(QColor color)
|
|||
return qsl("%1,%2,%3,%4").arg(color.red()).arg(color.green()).arg(color.blue()).arg(color.alpha());
|
||||
}
|
||||
|
||||
// Builds the QSS fragment shared by setConsoleBackgroundImage()/setWindowBackgroundImage()
|
||||
// for modes 1-4 ('border'/'center'/'tile'/'style'). Mode 5 ('cover') is not stylesheet-based
|
||||
// (Qt's border-image/background-image have no scale-to-fill option) and is handled separately.
|
||||
static QString buildBackgroundImageStyleSheet(const QString& objectName, const QColor& bgColor, int mode, const QString& imgPath)
|
||||
{
|
||||
if (mode == 1) {
|
||||
return qsl("QWidget#%1{background-color: rgba(%2); border-image: url(%3);}").arg(objectName, getColorCode(bgColor), imgPath);
|
||||
} else if (mode == 2) {
|
||||
return qsl("QWidget#%1{background-color: rgba(%2); background-image: url(%3); background-repeat: no-repeat; background-position: center; background-origin: margin;}")
|
||||
.arg(objectName, getColorCode(bgColor), imgPath);
|
||||
} else if (mode == 3) {
|
||||
return qsl("QWidget#%1{background-color: rgba(%2); background-image: url(%3);}").arg(objectName, getColorCode(bgColor), imgPath);
|
||||
} else if (mode == 4) {
|
||||
return qsl("QWidget#%1{background-color: rgba(%2); %3}").arg(objectName, getColorCode(bgColor), imgPath);
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
void TConsole::changeColors()
|
||||
{
|
||||
if (mType == CentralDebugConsole) {
|
||||
|
|
@ -1056,6 +1094,7 @@ void TConsole::changeColors()
|
|||
mCommandFgColor = mpHost->mCommandFgColor;
|
||||
mCommandBgColor = mpHost->mCommandBgColor;
|
||||
mFormatCurrent.setColors(mpHost->mFgColor, mpHost->mBgColor);
|
||||
updateMainFrameTransparency();
|
||||
} else {
|
||||
Q_ASSERT_X(false, "TConsole::changeColors()", "invalid TConsole type detected");
|
||||
}
|
||||
|
|
@ -1517,25 +1556,10 @@ void TConsole::setFontSize(int size)
|
|||
|
||||
bool TConsole::setConsoleBackgroundImage(const QString& imgPath, int mode)
|
||||
{
|
||||
QColor bgColor;
|
||||
QString styleSheet;
|
||||
const QColor bgColor = (mType == MainConsole) ? mpHost->mBgColor : mBgColor;
|
||||
|
||||
if (mType == MainConsole) {
|
||||
bgColor = mpHost->mBgColor;
|
||||
} else {
|
||||
bgColor = mBgColor;
|
||||
}
|
||||
|
||||
if (mode == 1) {
|
||||
styleSheet = qsl("QWidget#MainDisplay{background-color: rgba(%1); border-image: url(%2);}").arg(getColorCode(bgColor), imgPath);
|
||||
} else if (mode == 2) {
|
||||
styleSheet = qsl("QWidget#MainDisplay{background-color: rgba(%1); background-image: url(%2); background-repeat: no-repeat; background-position: center; background-origin: margin;}")
|
||||
.arg(getColorCode(bgColor), imgPath);
|
||||
} else if (mode == 3) {
|
||||
styleSheet = qsl("QWidget#MainDisplay{background-color: rgba(%1); background-image: url(%2);}").arg(getColorCode(bgColor), imgPath);
|
||||
} else if (mode == 4) {
|
||||
styleSheet = qsl("QWidget#MainDisplay{background-color: rgba(%1); %2}").arg(getColorCode(bgColor), imgPath);
|
||||
} else {
|
||||
const QString styleSheet = buildBackgroundImageStyleSheet(qsl("MainDisplay"), bgColor, mode, imgPath);
|
||||
if (styleSheet.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
mpMainDisplay->setStyleSheet(styleSheet);
|
||||
|
|
@ -1551,6 +1575,89 @@ bool TConsole::resetConsoleBackgroundImage()
|
|||
return true;
|
||||
}
|
||||
|
||||
bool TConsole::setWindowBackgroundImage(const QString& imgPath, int mode)
|
||||
{
|
||||
if (!mpWindowBackground) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mode == 5) {
|
||||
QPixmap pixmap(imgPath);
|
||||
if (pixmap.isNull()) {
|
||||
return false;
|
||||
}
|
||||
mWindowBgSourcePixmap = pixmap;
|
||||
mpWindowBackground->setStyleSheet(QString());
|
||||
updateWindowBackgroundCoverPixmap();
|
||||
} else {
|
||||
const QColor bgColor = mpHost ? mpHost->mBgColor : QColorConstants::Black;
|
||||
const QString styleSheet = buildBackgroundImageStyleSheet(qsl("WindowBackground"), bgColor, mode, imgPath);
|
||||
if (styleSheet.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
mWindowBgSourcePixmap = QPixmap();
|
||||
mpWindowBackground->setAutoFillBackground(false);
|
||||
mpWindowBackground->setPalette(QPalette());
|
||||
mpWindowBackground->setStyleSheet(styleSheet);
|
||||
}
|
||||
|
||||
mWindowBgImageMode = mode;
|
||||
mWindowBgImagePath = imgPath;
|
||||
updateMainFrameTransparency();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TConsole::resetWindowBackgroundImage()
|
||||
{
|
||||
if (!mpWindowBackground) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mWindowBgImageMode = 0;
|
||||
mWindowBgImagePath.clear();
|
||||
mWindowBgSourcePixmap = QPixmap();
|
||||
mpWindowBackground->setStyleSheet(QString());
|
||||
mpWindowBackground->setAutoFillBackground(false);
|
||||
mpWindowBackground->setPalette(QPalette());
|
||||
updateMainFrameTransparency();
|
||||
return true;
|
||||
}
|
||||
|
||||
void TConsole::updateMainFrameTransparency()
|
||||
{
|
||||
if (mType != MainConsole || !mpMainFrame) {
|
||||
return;
|
||||
}
|
||||
|
||||
QPalette framePalette;
|
||||
framePalette.setColor(QPalette::Text, QColor(Qt::black));
|
||||
framePalette.setColor(QPalette::Highlight, QColor(55, 55, 255));
|
||||
framePalette.setColor(QPalette::Window, mWindowBgImageMode ? QColor(0, 0, 0, 0) : QColor(0, 0, 0, 255));
|
||||
mpMainFrame->setPalette(framePalette);
|
||||
mpMainFrame->setAutoFillBackground(true);
|
||||
}
|
||||
|
||||
// Simulates CSS "cover" since QT stylesheets do not support it
|
||||
void TConsole::updateWindowBackgroundCoverPixmap()
|
||||
{
|
||||
if (!mpWindowBackground || mWindowBgSourcePixmap.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QSize targetSize = mpWindowBackground->size();
|
||||
if (targetSize.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QPixmap scaled = mWindowBgSourcePixmap.scaled(targetSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
|
||||
const QRect cropRect(qMax(0, (scaled.width() - targetSize.width()) / 2), qMax(0, (scaled.height() - targetSize.height()) / 2), targetSize.width(), targetSize.height());
|
||||
|
||||
QPalette palette;
|
||||
palette.setBrush(QPalette::Window, QBrush(scaled.copy(cropRect)));
|
||||
mpWindowBackground->setPalette(palette);
|
||||
mpWindowBackground->setAutoFillBackground(true);
|
||||
}
|
||||
|
||||
void TConsole::setCmdVisible(bool isVisible)
|
||||
{
|
||||
const QSizePolicy sizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
#include <QElapsedTimer>
|
||||
#include <QFont>
|
||||
#include <QIcon>
|
||||
#include <QPixmap>
|
||||
#include <QPointer>
|
||||
#include <QSaveFile>
|
||||
#include <QWidget>
|
||||
|
|
@ -290,6 +291,10 @@ public:
|
|||
void setFontName(const QString& fontName);
|
||||
bool setConsoleBackgroundImage(const QString&, int);
|
||||
bool resetConsoleBackgroundImage();
|
||||
bool setWindowBackgroundImage(const QString&, int);
|
||||
bool resetWindowBackgroundImage();
|
||||
void updateMainFrameTransparency();
|
||||
void updateWindowBackgroundCoverPixmap();
|
||||
void setLink(const QStringList& linkFunction, const QStringList& linkHint, const QVector<int> linkReference = QVector<int>());
|
||||
// Cannot be called setAttributes as that would mask an inherited method
|
||||
void setDisplayAttributes(const TChar::AttributeFlags, const bool);
|
||||
|
|
@ -382,6 +387,7 @@ public:
|
|||
QWidget* mpMainFrame = nullptr;
|
||||
QWidget* mpRightToolBar = nullptr;
|
||||
QWidget* mpMainDisplay = nullptr;
|
||||
QWidget* mpWindowBackground = nullptr;
|
||||
|
||||
QPointer<dlgMapper> mpMapper;
|
||||
|
||||
|
|
@ -421,6 +427,9 @@ public:
|
|||
QWidget* mpButtonMainLayer = nullptr;
|
||||
int mBgImageMode = 0;
|
||||
QString mBgImagePath;
|
||||
int mWindowBgImageMode = 0;
|
||||
QString mWindowBgImagePath;
|
||||
QPixmap mWindowBgSourcePixmap;
|
||||
bool mHScrollBarEnabled = false;
|
||||
ControlCharacterMode mControlCharacter = ControlCharacterMode::AsIs;
|
||||
QVideoWidget* mpVideoWidget = nullptr;
|
||||
|
|
|
|||
|
|
@ -2229,13 +2229,25 @@ int TLuaInterpreter::resetCmdLineAction(lua_State* L)
|
|||
int TLuaInterpreter::resetBackgroundImage(lua_State* L)
|
||||
{
|
||||
QString windowName = qsl("main");
|
||||
bool fullWindow = false;
|
||||
const int n = lua_gettop(L);
|
||||
if (n > 0) {
|
||||
int counter = 1;
|
||||
if (n > 0 && lua_type(L, 1) == LUA_TSTRING) {
|
||||
windowName = getVerifiedString(L, __func__, 1, "console name");
|
||||
counter++;
|
||||
}
|
||||
|
||||
if (counter <= n) {
|
||||
fullWindow = getVerifiedBool(L, __func__, counter, "fullWindow");
|
||||
counter++;
|
||||
}
|
||||
|
||||
if (fullWindow && !(windowName.isEmpty() || windowName.compare(qsl("main"), Qt::CaseSensitive) == 0)) {
|
||||
return warnArgumentValue(L, __func__, qsl("the full window background can only be reset on the main console"));
|
||||
}
|
||||
|
||||
Host* host = &getHostFromLua(L);
|
||||
if (!host->resetBackgroundImage(windowName)) {
|
||||
if (!host->resetBackgroundImage(windowName, fullWindow)) {
|
||||
return warnArgumentValue(L, __func__, qsl("console '%1' not found").arg(windowName));
|
||||
}
|
||||
lua_pushboolean(L, true);
|
||||
|
|
@ -2520,6 +2532,7 @@ int TLuaInterpreter::setBackgroundImage(lua_State* L)
|
|||
QString windowName = qsl("main");
|
||||
QString imgPath;
|
||||
int mode = 1;
|
||||
bool fullWindow = false;
|
||||
int counter = 1;
|
||||
const int n = lua_gettop(L);
|
||||
if (n > 1 && lua_type(L, 2) == LUA_TSTRING) {
|
||||
|
|
@ -2530,16 +2543,30 @@ int TLuaInterpreter::setBackgroundImage(lua_State* L)
|
|||
imgPath = getVerifiedString(L, __func__, counter, "image path");
|
||||
counter++;
|
||||
|
||||
if (n > 2 || (counter == 2 && n > 1)) {
|
||||
if (counter <= n) {
|
||||
mode = getVerifiedInt(L, __func__, counter, "mode");
|
||||
counter++;
|
||||
}
|
||||
|
||||
if (mode < 1 || mode > 4) {
|
||||
return warnArgumentValue(L, __func__, qsl("%1 is not a valid mode! Valid modes are 1 'border', 2 'center', 3 'tile', 4 'style'").arg(mode));
|
||||
if (counter <= n) {
|
||||
fullWindow = getVerifiedBool(L, __func__, counter, "fullWindow");
|
||||
counter++;
|
||||
}
|
||||
|
||||
if (mode < 1 || mode > 5) {
|
||||
return warnArgumentValue(L, __func__, qsl("%1 is not a valid mode! Valid modes are 1 'border', 2 'center', 3 'tile', 4 'style', 5 'cover'").arg(mode));
|
||||
}
|
||||
|
||||
if (mode == 5 && !fullWindow) {
|
||||
return warnArgumentValue(L, __func__, qsl("mode 'cover' is not supported for the main display - pass true as the 4th argument to apply it to the full window background instead"));
|
||||
}
|
||||
|
||||
if (fullWindow && !(windowName.isEmpty() || windowName.compare(qsl("main"), Qt::CaseSensitive) == 0)) {
|
||||
return warnArgumentValue(L, __func__, qsl("the full window background can only be used with the main console"));
|
||||
}
|
||||
|
||||
Host* host = &getHostFromLua(L);
|
||||
if (!host->setBackgroundImage(windowName, imgPath, mode)) {
|
||||
if (!host->setBackgroundImage(windowName, imgPath, mode, fullWindow)) {
|
||||
return warnArgumentValue(L, __func__, qsl("console or label '%1' not found").arg(windowName));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2536,15 +2536,21 @@ mudlet.BgImageMode ={
|
|||
["center"] = 2,
|
||||
["tile"] = 3,
|
||||
["style"] = 4,
|
||||
["cover"] = 5,
|
||||
}
|
||||
|
||||
local setConsoleBackgroundImageLayer = setBackgroundImage
|
||||
function setBackgroundImage(...)
|
||||
local mode = arg[arg.n]
|
||||
-- fullWindow, if given, is always the last argument - mode is whatever precedes it
|
||||
local modePos = arg.n
|
||||
if type(arg[arg.n]) == "boolean" then
|
||||
modePos = arg.n - 1
|
||||
end
|
||||
local mode = arg[modePos]
|
||||
if type(mode) == "string" then
|
||||
mode = mudlet.BgImageMode[mode] or mode
|
||||
end
|
||||
arg[arg.n] = mode
|
||||
arg[modePos] = mode
|
||||
return setConsoleBackgroundImageLayer(unpack(arg))
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue