add: widget state getters for titles, stylesheets, tooltips and scroll bars (#9645)

#### Brief overview of PR changes/additions

- Seven state getters that are the inverse of setters we already ship:
`getUserWindowTitle`, `getUserWindowStyleSheet`, `getCmdLineStyleSheet`,
`getLabelToolTip`, `getScrollBarVisible`, `getMapWindowTitle` and
`getMapWidgetGeometry`
- Each returns nil plus a message when the window, label or map widget
it names does not exist, reusing the matching setter's wording so the
pair reports the same problems the same way
- 39 specs added to the existing `UI_spec.lua` and `Mapper_spec.lua`

#### Motivation for adding to Mudlet

#9630's audit left 11 Geyser/UI rows untestable purely because the state
those functions set could not be read back; this tranche unblocks them
exactly as #9528's getters unblocked the geometry specs. Scripts get the
same readback symmetry as a side effect.

#### Other info (issues closed, discussion etc)

One deliberate behaviour change: `enableScrollBar`/`disableScrollBar`
now record what they were asked for, so `getScrollBarVisible` answers
for a profile that is not the front tab (whose whole console Mudlet
hides) instead of reporting every background profile's scroll bar as
gone. Wiki pages for the seven functions to follow in Area 51.

**Test case:** busted 1868 passed / 0 failed / 0 errors / 17 pending
(baseline 1829), green twice on the same isolated profile, plus ctest;
31 of the new specs verified by breaking the getters - wrong return
values fail 20, making the not-found branches succeed fails 7, and
dropping the empty-name and nil handling fails 8 more.

Assisted-by: Claude:claude-opus-5
This commit is contained in:
Vadim Peretokin 2026-08-05 06:49:23 +02:00 committed by GitHub
parent 5b33c2f35f
commit 7bb20fa2ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 611 additions and 6 deletions

View file

@ -3486,6 +3486,15 @@ std::pair<bool, QString> Host::setMapperTitle(const QString& title)
return {true, QString()};
}
std::optional<QString> Host::getMapperTitle() const
{
if (!mpConsole || !mpConsole->mpDockableMapWidget) {
return {};
}
return {mpConsole->mpDockableMapWidget->windowTitle()};
}
std::pair<int, QString> Host::createMapView(int areaId)
{
if (!mpMap) {
@ -4250,6 +4259,20 @@ std::pair<bool, QString> Host::openMapWidget(const QString& area, int x, int y,
return {false, qsl(R"("docking option "%1" not available. available docking options are "t" top, "b" bottom, "r" right, "l" left and "f" floating")").arg(area)};
}
// The inverse of moveMapWidget()/resizeMapWidget(), which reach the dock widget
// through openMapWidget(). pos()/size() rather than geometry() for the same
// reason as Host::windowGeometry(): they are what move()/resize() were given,
// while a floating dock's geometry() reports the client area instead.
std::optional<QRect> Host::mapWidgetGeometry() const
{
if (!mpConsole || !mpConsole->mpDockableMapWidget) {
return {};
}
auto pM = mpConsole->mpDockableMapWidget;
return {QRect(pM->pos(), pM->size())};
}
std::pair<bool, QString> Host::closeMapWidget()
{
if (!mpConsole) {

View file

@ -395,6 +395,7 @@ public:
void setSearchOptions(const dlgTriggerEditor::SearchOptions);
void setBufferSearchOptions(const TConsole::SearchOptions);
std::pair<bool, QString> setMapperTitle(const QString&);
std::optional<QString> getMapperTitle() const;
// Multiple map views support
std::pair<int, QString> createMapView(int areaId = 0);
@ -429,6 +430,7 @@ public:
std::pair<bool, QString> setWindow(const QString& windowname, const QString& name, int x1, int y1, bool show);
std::pair<bool, QString> openMapWidget(const QString& area, int x, int y, int width, int height);
std::pair<bool, QString> closeMapWidget();
std::optional<QRect> mapWidgetGeometry() const;
bool closeWindow(const QString&);
bool echoWindow(const QString&, const QString&);
bool pasteWindow(const QString& name);

View file

@ -589,6 +589,10 @@ TConsole::TConsole(Host* pH, const QString& name, const ConsoleType type, QWidge
mHScrollBarEnabled = true;
}
// a Buffer is never displayed and the three types below start with their
// scroll bar hidden, so only the main and debug consoles begin with one
mScrollBarEnabled = !(mType & (ErrorConsole | SubConsole | UserWindow | Buffer));
if (mType & (ErrorConsole | SubConsole | UserWindow)) {
mpScrollBar->hide();
mLowerPane->hide();
@ -1948,10 +1952,20 @@ void TConsole::setCommandFgColor(const QColor& newColor)
void TConsole::setScrollBarVisible(bool isVisible)
{
if (mpScrollBar) {
mScrollBarEnabled = isVisible;
mpScrollBar->setVisible(isVisible);
}
}
// Reports what enableScrollBar()/disableScrollBar() last asked for rather than
// QWidget::isVisible(): a profile that is not the front tab has its whole
// console hidden, which would otherwise make every background profile report
// its scroll bar as gone.
bool TConsole::getScrollBarVisible() const
{
return mScrollBarEnabled;
}
void TConsole::setHorizontalScrollBar(bool isEnabled)
{
if (mpHScrollBar) {

View file

@ -252,6 +252,7 @@ public:
void setCommandFgColor(const QColor&);
void setCommandFgColor(int, int, int, int);
void setScrollBarVisible(bool);
bool getScrollBarVisible() const;
void setHorizontalScrollBar(bool);
void setScrolling(const bool state);
bool getScrolling() const { return mScrollingEnabled; }
@ -432,6 +433,7 @@ public:
QString mWindowBgImagePath;
QPixmap mWindowBgSourcePixmap;
bool mHScrollBarEnabled = false;
bool mScrollBarEnabled = true;
ControlCharacterMode mControlCharacter = ControlCharacterMode::AsIs;
QVideoWidget* mpVideoWidget = nullptr;
QSplitter* commandSplitter = nullptr;

View file

@ -5371,6 +5371,7 @@ void TLuaInterpreter::initLuaGlobals()
lua_register(pGlobalLua, "getFontSize", TLuaInterpreter::getFontSize);
lua_register(pGlobalLua, "openUserWindow", TLuaInterpreter::openUserWindow);
lua_register(pGlobalLua, "setUserWindowTitle", TLuaInterpreter::setUserWindowTitle);
lua_register(pGlobalLua, "getUserWindowTitle", TLuaInterpreter::getUserWindowTitle);
lua_register(pGlobalLua, "echoUserWindow", TLuaInterpreter::echoUserWindow);
lua_register(pGlobalLua, "enableTimer", TLuaInterpreter::enableTimer);
lua_register(pGlobalLua, "disableTimer", TLuaInterpreter::disableTimer);
@ -5434,6 +5435,7 @@ void TLuaInterpreter::initLuaGlobals()
lua_register(pGlobalLua, "setTextEditTabMovesFocus", TLuaInterpreter::setTextEditTabMovesFocus);
lua_register(pGlobalLua, "deleteScrollBox", TLuaInterpreter::deleteScrollBox);
lua_register(pGlobalLua, "setLabelToolTip", TLuaInterpreter::setLabelToolTip);
lua_register(pGlobalLua, "getLabelToolTip", TLuaInterpreter::getLabelToolTip);
lua_register(pGlobalLua, "setLabelCursor", TLuaInterpreter::setLabelCursor);
lua_register(pGlobalLua, "setLabelCustomCursor", TLuaInterpreter::setLabelCustomCursor);
lua_register(pGlobalLua, "raiseWindow", TLuaInterpreter::raiseWindow);
@ -5463,6 +5465,7 @@ void TLuaInterpreter::initLuaGlobals()
lua_register(pGlobalLua, "setCmdLineAction", TLuaInterpreter::setCmdLineAction);
lua_register(pGlobalLua, "resetCmdLineAction", TLuaInterpreter::resetCmdLineAction);
lua_register(pGlobalLua, "setCmdLineStyleSheet", TLuaInterpreter::setCmdLineStyleSheet);
lua_register(pGlobalLua, "getCmdLineStyleSheet", TLuaInterpreter::getCmdLineStyleSheet);
lua_register(pGlobalLua, "setLabelClickCallback", TLuaInterpreter::setLabelClickCallback);
lua_register(pGlobalLua, "setLabelDoubleClickCallback", TLuaInterpreter::setLabelDoubleClickCallback);
lua_register(pGlobalLua, "setLabelReleaseCallback", TLuaInterpreter::setLabelReleaseCallback);
@ -5481,6 +5484,7 @@ void TLuaInterpreter::initLuaGlobals()
lua_register(pGlobalLua, "setWindow", TLuaInterpreter::setWindow);
lua_register(pGlobalLua, "openMapWidget", TLuaInterpreter::openMapWidget);
lua_register(pGlobalLua, "closeMapWidget", TLuaInterpreter::closeMapWidget);
lua_register(pGlobalLua, "getMapWidgetGeometry", TLuaInterpreter::getMapWidgetGeometry);
lua_register(pGlobalLua, "setTextFormat", TLuaInterpreter::setTextFormat);
lua_register(pGlobalLua, "getMainWindowSize", TLuaInterpreter::getMainWindowSize);
lua_register(pGlobalLua, "getUserWindowSize", TLuaInterpreter::getUserWindowSize);
@ -5555,6 +5559,7 @@ void TLuaInterpreter::initLuaGlobals()
lua_register(pGlobalLua, "setConsoleBufferSize", TLuaInterpreter::setConsoleBufferSize);
lua_register(pGlobalLua, "enableScrollBar", TLuaInterpreter::enableScrollBar);
lua_register(pGlobalLua, "disableScrollBar", TLuaInterpreter::disableScrollBar);
lua_register(pGlobalLua, "getScrollBarVisible", TLuaInterpreter::getScrollBarVisible);
lua_register(pGlobalLua, "enableHorizontalScrollBar", TLuaInterpreter::enableHorizontalScrollBar);
lua_register(pGlobalLua, "disableHorizontalScrollBar", TLuaInterpreter::disableHorizontalScrollBar);
lua_register(pGlobalLua, "enableCommandLine", TLuaInterpreter::enableCommandLine);
@ -5584,6 +5589,7 @@ void TLuaInterpreter::initLuaGlobals()
lua_register(pGlobalLua, "killAlias", TLuaInterpreter::killAlias);
lua_register(pGlobalLua, "setLabelStyleSheet", TLuaInterpreter::setLabelStyleSheet);
lua_register(pGlobalLua, "setUserWindowStyleSheet", TLuaInterpreter::setUserWindowStyleSheet);
lua_register(pGlobalLua, "getUserWindowStyleSheet", TLuaInterpreter::getUserWindowStyleSheet);
lua_register(pGlobalLua, "getTime", TLuaInterpreter::getTime);
lua_register(pGlobalLua, "getEpoch", TLuaInterpreter::getEpoch);
lua_register(pGlobalLua, "invokeFileDialog", TLuaInterpreter::invokeFileDialog);
@ -5876,6 +5882,7 @@ void TLuaInterpreter::initLuaGlobals()
lua_register(pGlobalLua, "getConnectionInfo", TLuaInterpreter::getConnectionInfo);
lua_register(pGlobalLua, "unzipAsync", TLuaInterpreter::unzipAsync);
lua_register(pGlobalLua, "setMapWindowTitle", TLuaInterpreter::setMapWindowTitle);
lua_register(pGlobalLua, "getMapWindowTitle", TLuaInterpreter::getMapWindowTitle);
lua_register(pGlobalLua, "getMudletInfo", TLuaInterpreter::getMudletInfo);
lua_register(pGlobalLua, "getMapBackgroundColor", TLuaInterpreter::getMapBackgroundColor);
lua_register(pGlobalLua, "setMapBackgroundColor", TLuaInterpreter::setMapBackgroundColor);

View file

@ -371,6 +371,7 @@ public:
static int getFontSize(lua_State*);
static int openUserWindow(lua_State*);
static int setUserWindowTitle(lua_State*);
static int getUserWindowTitle(lua_State*);
static int echoUserWindow(lua_State*);
static int clearUserWindow(lua_State*);
static int enableTimer(lua_State*);
@ -464,12 +465,14 @@ public:
static int setTextEditTabMovesFocus(lua_State*);
static int deleteScrollBox(lua_State*);
static int setLabelToolTip(lua_State*);
static int getLabelToolTip(lua_State*);
static int setLabelCursor(lua_State*);
static int setLabelCustomCursor(lua_State*);
static int moveWindow(lua_State*);
static int setWindow(lua_State*);
static int openMapWidget(lua_State*);
static int closeMapWidget(lua_State*);
static int getMapWidgetGeometry(lua_State*);
static int setTextFormat(lua_State*);
static int setBackgroundImage(lua_State*);
static int resetBackgroundImage(lua_State*);
@ -486,6 +489,7 @@ public:
static int setCmdLineAction(lua_State*);
static int resetCmdLineAction(lua_State*);
static int setCmdLineStyleSheet(lua_State*);
static int getCmdLineStyleSheet(lua_State*);
static int getImageSize(lua_State*);
static int setLabelDoubleClickCallback(lua_State*);
static int setLabelReleaseCallback(lua_State*);
@ -560,6 +564,7 @@ public:
static int getConsoleBufferSize(lua_State*);
static int setConsoleBufferSize(lua_State*);
static int enableScrollBar(lua_State*);
static int getScrollBarVisible(lua_State*);
static int disableScrollBar(lua_State*);
static int disableHorizontalScrollBar(lua_State*);
static int enableHorizontalScrollBar(lua_State*);
@ -599,6 +604,7 @@ public:
static int killAlias(lua_State*);
static int permBeginOfLineStringTrigger(lua_State*);
static int setUserWindowStyleSheet(lua_State*);
static int getUserWindowStyleSheet(lua_State*);
static int getTime(lua_State*);
static int getEpoch(lua_State*);
static int invokeFileDialog(lua_State*);
@ -727,6 +733,7 @@ public:
static int getConnectionInfo(lua_State*);
static int unzipAsync(lua_State*);
static int setMapWindowTitle(lua_State*);
static int getMapWindowTitle(lua_State*);
static int getMudletInfo(lua_State*);
static int getMapBackgroundColor(lua_State*);
static int setMapBackgroundColor(lua_State*);

View file

@ -951,6 +951,22 @@ int TLuaInterpreter::closeMapWidget(lua_State* L)
return 1;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#getMapWidgetGeometry
int TLuaInterpreter::getMapWidgetGeometry(lua_State* L)
{
const Host& host = getHostFromLua(L);
if (auto geometry = host.mapWidgetGeometry()) {
lua_pushnumber(L, geometry->x());
lua_pushnumber(L, geometry->y());
lua_pushnumber(L, geometry->width());
lua_pushnumber(L, geometry->height());
return 4;
}
return warnArgumentValue(L, __func__, "no floating/dockable type map window found");
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#connectExitStub
int TLuaInterpreter::connectExitStub(lua_State* L)
{

View file

@ -1117,6 +1117,15 @@ int TLuaInterpreter::enableScrollBar(lua_State* L)
return 0;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#getScrollBarVisible
int TLuaInterpreter::getScrollBarVisible(lua_State* L)
{
const QString windowName{WINDOW_NAME(L, 1)};
auto console = CONSOLE(L, windowName);
lua_pushboolean(L, console->getScrollBarVisible());
return 1;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#enableTimeStamps
int TLuaInterpreter::enableTimeStamps(lua_State* L)
{
@ -2982,6 +2991,27 @@ int TLuaInterpreter::setCmdLineStyleSheet(lua_State* L)
return 1;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#getCmdLineStyleSheet
int TLuaInterpreter::getCmdLineStyleSheet(lua_State* L)
{
// an explicit nil means "the main command line", as it does for the window
// name of every other getter that takes an optional one
const bool hasName = lua_gettop(L) > 0 && !lua_isnil(L, 1);
if (hasName && !checkStringArg(L, __func__, 1, "command line name")) {
return lua_error(L);
}
const QString name = hasName ? QString{lua_tostring(L, 1)} : qsl("main");
const Host& host = getHostFromLua(L);
if (auto styleSheet = host.mpConsole->getCmdLineStyleSheet(name)) {
lua_pushstring(L, styleSheet->toUtf8().constData());
return 1;
}
return warnArgumentValue(L, __func__, qsl("command-line name '%1' not found").arg(name));
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#setFont
int TLuaInterpreter::setFont(lua_State* L)
{
@ -3124,6 +3154,23 @@ int TLuaInterpreter::setLabelToolTip(lua_State* L)
return 1;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#getLabelToolTip
int TLuaInterpreter::getLabelToolTip(lua_State* L)
{
const QString labelName = getVerifiedString(L, __func__, 1, "label name");
if (labelName.isEmpty()) {
return warnArgumentValue(L, __func__, "a label cannot have an empty string as its name");
}
const Host& host = getHostFromLua(L);
if (auto toolTip = host.mpConsole->getLabelToolTip(labelName)) {
lua_pushstring(L, toolTip->toUtf8().constData());
return 1;
}
return warnArgumentValue(L, __func__, qsl("label name '%1' not found").arg(labelName));
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#setLabelClickCallback
int TLuaInterpreter::setLabelClickCallback(lua_State* L)
{
@ -3296,6 +3343,19 @@ int TLuaInterpreter::setMapWindowTitle(lua_State* L)
return 1;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#getMapWindowTitle
int TLuaInterpreter::getMapWindowTitle(lua_State* L)
{
const Host& host = getHostFromLua(L);
if (auto title = host.getMapperTitle()) {
lua_pushstring(L, title->toUtf8().constData());
return 1;
}
return warnArgumentValue(L, __func__, "no floating/dockable type map window found");
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#setMovie
int TLuaInterpreter::setMovie(lua_State* L)
{
@ -3595,6 +3655,21 @@ int TLuaInterpreter::setUserWindowTitle(lua_State* L)
return 1;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#getUserWindowTitle
int TLuaInterpreter::getUserWindowTitle(lua_State* L)
{
const QString name = getVerifiedString(L, __func__, 1, "name");
const Host& host = getHostFromLua(L);
auto [success, result] = host.mpConsole->getUserWindowTitle(name);
if (!success) {
return warnArgumentValue(L, __func__, result);
}
lua_pushstring(L, result.toUtf8().constData());
return 1;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#setUserWindowStyleSheet
int TLuaInterpreter::setUserWindowStyleSheet(lua_State* L)
{
@ -3613,6 +3688,23 @@ int TLuaInterpreter::setUserWindowStyleSheet(lua_State* L)
return 1;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#getUserWindowStyleSheet
int TLuaInterpreter::getUserWindowStyleSheet(lua_State* L)
{
const QString userWindowName = getVerifiedString(L, __func__, 1, "userwindow name");
if (userWindowName.isEmpty()) {
return warnArgumentValue(L, __func__, "a userwindow cannot have an empty string as its name");
}
const Host& host = getHostFromLua(L);
if (auto styleSheet = host.mpConsole->getUserWindowStyleSheet(userWindowName)) {
lua_pushstring(L, styleSheet->toUtf8().constData());
return 1;
}
return warnArgumentValue(L, __func__, qsl("userwindow name '%1' not found").arg(userWindowName));
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#setWindow
int TLuaInterpreter::setWindow(lua_State* L)
{

View file

@ -145,6 +145,16 @@ std::optional<QSize> TMainConsole::getLabelSizeHint(const QString& name) const
return {};
}
std::optional<QString> TMainConsole::getLabelToolTip(const QString& name) const
{
auto pL = mLabelMap.value(name);
if (!pL) {
return {};
}
return {pL->toolTip()};
}
// NOLINTNEXTLINE(readability-make-member-function-const)
std::pair<bool, QString> TMainConsole::setUserWindowStyleSheet(const QString& name, const QString& userWindowStyleSheet)
{
@ -160,6 +170,16 @@ std::pair<bool, QString> TMainConsole::setUserWindowStyleSheet(const QString& na
return {false, qsl("userwindow name '%1' not found").arg(name)};
}
std::optional<QString> TMainConsole::getUserWindowStyleSheet(const QString& name) const
{
auto pW = mDockWidgetMap.value(name);
if (!pW) {
return {};
}
return {pW->styleSheet()};
}
std::pair<bool, QString> TMainConsole::setCmdLineStyleSheet(const QString& name, const QString& styleSheet)
{
if (name.isEmpty() || !name.compare(qsl("main"))) {
@ -175,6 +195,23 @@ std::pair<bool, QString> TMainConsole::setCmdLineStyleSheet(const QString& name,
return {false, qsl("command-line name '%1' not found").arg(name)};
}
std::optional<QString> TMainConsole::getCmdLineStyleSheet(const QString& name) const
{
if (name.isEmpty() || !name.compare(qsl("main"))) {
if (auto pMain = mpHost->mpConsole->mpCommandLine) {
return {pMain->styleSheet()};
}
return {};
}
auto pN = mSubCommandLineMap.value(name);
if (!pN) {
return {};
}
return {pN->styleSheet()};
}
void TMainConsole::toggleLogging(bool isMessageEnabled)
{
const auto loggingPath = mudlet::getMudletPath(enums::profileDataItemPath, mpHost->getName(), qsl("autolog"));
@ -1375,6 +1412,32 @@ std::pair<bool, QString> TMainConsole::setUserWindowTitle(const QString& name, c
return {false, qsl("internal error: TConsole \"%1\" is marked as a user window but does not have a TDockWidget to contain it").arg(name)};
}
// The title is in .second when .first is true, otherwise .second is why there
// is none. Mirrors setUserWindowTitle's checks in the same order and words, so
// that a miniconsole sharing the name is not reported as a missing window.
std::pair<bool, QString> TMainConsole::getUserWindowTitle(const QString& name) const
{
if (name.isEmpty()) {
return {false, qsl("a user window cannot have an empty string as its name")};
}
auto pC = mSubConsoleMap.value(name);
if (!pC) {
return {false, qsl("user window name '%1' not found").arg(name)};
}
if (pC->getType() != UserWindow) {
return {false, qsl("\"%1\" is not a user window").arg(name)};
}
auto pD = mDockWidgetMap.value(name);
if (!pD) {
return {false, qsl("internal error: TConsole \"%1\" is marked as a user window but does not have a TDockWidget to contain it").arg(name)};
}
return {true, pD->windowTitle()};
}
bool TMainConsole::setTextFormat(const QString& name, const QColor& fgColor, const QColor& bgColor, const TChar::AttributeFlags& flags)
{
if (name.isEmpty() || name.compare(qsl("main"), Qt::CaseSensitive) == 0) {

View file

@ -70,7 +70,9 @@ public:
QString getCurrentLine(const std::string&);
TConsole* createBuffer(const QString& name);
std::pair<bool, QString> setUserWindowStyleSheet(const QString& name, const QString& userWindowStyleSheet);
std::optional<QString> getUserWindowStyleSheet(const QString& name) const;
std::pair<bool, QString> setUserWindowTitle(const QString& name, const QString& text);
std::pair<bool, QString> getUserWindowTitle(const QString& name) const;
bool setTextFormat(const QString& name, const QColor& fgColor, const QColor& bgColor, const TChar::AttributeFlags& flags);
TLabel* createLabel(const QString& windowname, const QString& name, int x, int y, int width, int height, bool fillBackground, bool clickThrough = false);
std::pair<bool, QString> createMapper(const QString& windowname, int, int, int, int);
@ -78,6 +80,7 @@ public:
std::pair<bool, QString> createTextBox(const QString& windowname, const QString& name, int, int, int, int);
QSize getUserWindowSize(const QString& windowname) const;
std::pair<bool, QString> setCmdLineStyleSheet(const QString& name, const QString& styleSheet);
std::optional<QString> getCmdLineStyleSheet(const QString& name) const;
std::pair<bool, QString> setLabelStyleSheet(const QString& name, const QString& stylesheet);
std::optional<QString> getLabelStyleSheet(const QString& name) const;
std::optional<QSize> getLabelSizeHint(const QString& name) const;
@ -87,6 +90,7 @@ public:
std::pair<bool, QString> deleteTextBox(const QString&);
std::pair<bool, QString> deleteScrollBox(const QString&);
std::pair<bool, QString> setLabelToolTip(const QString& name, const QString& text, double duration);
std::optional<QString> getLabelToolTip(const QString& name) const;
std::pair<bool, QString> setLabelCursor(const QString& name, int shape);
std::pair<bool, QString> setLabelCustomCursor(const QString& name, const QString& pixMapLocation, int hotX, int hotY);
bool setBackgroundImage(const QString& name, const QString& path);

View file

@ -27,6 +27,27 @@ describe("Tests map events and menus before the map widget is opened", function(
assert.is_nil(getMapMenus()["PreWidgetMenu"])
end)
-- nothing can destroy the map widget again once it exists, so a second
-- runTests in the same session inherits one and these two have nothing left
-- to observe
it("should report that there is no map widget to read a title from", function()
local title, err = getMapWindowTitle()
if title then
pending("the map widget is already open in this session")
return
end
assert.are.equal("no floating/dockable type map window found", err)
end)
it("should report that there is no map widget to read a geometry from", function()
local x, err = getMapWidgetGeometry()
if x then
pending("the map widget is already open in this session")
return
end
assert.are.equal("no floating/dockable type map window found", err)
end)
it("should retain a registration for when the widget opens later", function()
assert.is_true(addMapEvent("preWidgetKeptEvent", "myEvent", "", "Kept Event"))
end)

View file

@ -2650,10 +2650,10 @@ describe("Window state getters", function()
end)
it("returns nil and a message naming an unknown window", function()
local result, err = getWindowGeometry("wsgNoSuchWindow")
local result, err = getWindowGeometry("wdgNoSuchWindow")
assert.is_nil(result)
assert.are.equal("string", type(err))
assert.is_truthy(err:find("wsgNoSuchWindow", 1, true))
assert.is_truthy(err:find("wdgNoSuchWindow", 1, true))
end)
it("returns nil and a message for the main window", function()
@ -2721,10 +2721,10 @@ describe("Window state getters", function()
end)
it("returns nil and a message naming an unknown window", function()
local result, err = windowVisible("wsgNoSuchWindow")
local result, err = windowVisible("wdgNoSuchWindow")
assert.is_nil(result)
assert.are.equal("string", type(err))
assert.is_truthy(err:find("wsgNoSuchWindow", 1, true))
assert.is_truthy(err:find("wdgNoSuchWindow", 1, true))
end)
it("returns nil and a message for the main window", function()
@ -2752,10 +2752,10 @@ describe("Window state getters", function()
end)
it("returns nil and a message naming an unknown label", function()
local result, err = getLabelText("wsgNoSuchLabel")
local result, err = getLabelText("wdgNoSuchLabel")
assert.is_nil(result)
assert.are.equal("string", type(err))
assert.is_truthy(err:find("wsgNoSuchLabel", 1, true))
assert.is_truthy(err:find("wdgNoSuchLabel", 1, true))
end)
it("returns nil and a message for a non-label window", function()
@ -4089,3 +4089,357 @@ describe("Window and label state", function()
end)
end)
end)
-- Widget state getters: titles, stylesheets, tooltips, scroll bars and the map
-- widget's geometry, all of which could previously only be set. Self-contained
-- top-level block kept at the tail of the file; do not interleave it with the
-- blocks above.
describe("Widget state getters", function()
-- user windows and the map widget cannot be deleted from Lua, only hidden,
-- so keep the names unique per run
local suffix = ("-%d-%d"):format(os.time(), math.random(100000))
local function name(base)
return base .. suffix
end
local userWindow = name("wdgUserWindow")
local label = name("wdgLabel")
local console = name("wdgConsole")
local cmdLine = name("wdgCmdLine")
setup(function()
-- loadLayout is off so a saved layout cannot move the window under us
openUserWindow(userWindow, false)
createLabel(label, 10, 20, 100, 50, 1)
createMiniConsole(console, 30, 40, 300, 150)
createCommandLine(cmdLine, 15, 25, 140, 35)
end)
teardown(function()
deleteLabel(label)
deleteMiniConsole(console)
deleteCommandLine(cmdLine)
hideWindow(userWindow)
end)
describe("getUserWindowTitle", function()
teardown(function()
resetUserWindowTitle(userWindow)
end)
it("returns the title set by setUserWindowTitle", function()
assert.is_true(setUserWindowTitle(userWindow, "A user window title"))
assert.are.equal("A user window title", getUserWindowTitle(userWindow))
end)
it("round-trips an updated title", function()
setUserWindowTitle(userWindow, "first title")
assert.are.equal("first title", getUserWindowTitle(userWindow))
setUserWindowTitle(userWindow, "second title")
assert.are.equal("second title", getUserWindowTitle(userWindow))
end)
it("reports the generated default title after resetUserWindowTitle", function()
setUserWindowTitle(userWindow, "not the default")
assert.is_true(resetUserWindowTitle(userWindow))
local title = getUserWindowTitle(userWindow)
assert.are.equal("string", type(title))
assert.is_truthy(title:find(getProfileName(), 1, true))
assert.is_truthy(title:find(userWindow, 1, true))
end)
it("returns nil and a message naming an unknown user window", function()
local unknown = name("wdgNoSuchUserWindow")
local ok, err = getUserWindowTitle(unknown)
assert.is_nil(ok)
assert.are.equal(("user window name '%s' not found"):format(unknown), err)
end)
it("says a miniconsole of that name is not a user window", function()
-- the same distinction setUserWindowTitle makes, so a script is not told
-- a name is free when it is already taken by something else
local ok, err = getUserWindowTitle(console)
assert.is_nil(ok)
assert.are.equal(('"%s" is not a user window'):format(console), err)
end)
it("rejects an empty name the way setUserWindowTitle does", function()
local ok, err = getUserWindowTitle("")
assert.is_nil(ok)
assert.are.equal("a user window cannot have an empty string as its name", err)
end)
it("errors when called without a name", function()
assert.has_error(function() getUserWindowTitle() end)
end)
end)
describe("getUserWindowStyleSheet", function()
teardown(function()
setUserWindowStyleSheet(userWindow, "")
end)
it("returns the stylesheet set by setUserWindowStyleSheet", function()
local css = "background-color: rgb(11,22,33);"
assert.is_true(setUserWindowStyleSheet(userWindow, css))
assert.are.equal(css, getUserWindowStyleSheet(userWindow))
end)
it("round-trips an updated stylesheet", function()
setUserWindowStyleSheet(userWindow, "background-color: rgb(1,2,3);")
assert.are.equal("background-color: rgb(1,2,3);", getUserWindowStyleSheet(userWindow))
setUserWindowStyleSheet(userWindow, "background-color: rgb(4,5,6);")
assert.are.equal("background-color: rgb(4,5,6);", getUserWindowStyleSheet(userWindow))
end)
it("reports an empty stylesheet once it is cleared", function()
setUserWindowStyleSheet(userWindow, "background-color: rgb(7,8,9);")
assert.is_true(setUserWindowStyleSheet(userWindow, ""))
assert.are.equal("", getUserWindowStyleSheet(userWindow))
end)
it("returns nil and a message naming an unknown user window", function()
local unknown = name("wdgNoSuchUserWindow")
local ok, err = getUserWindowStyleSheet(unknown)
assert.is_nil(ok)
assert.are.equal(("userwindow name '%s' not found"):format(unknown), err)
end)
it("rejects an empty name the way setUserWindowStyleSheet does", function()
local ok, err = getUserWindowStyleSheet("")
assert.is_nil(ok)
assert.are.equal("a userwindow cannot have an empty string as its name", err)
end)
it("errors when called without a name", function()
assert.has_error(function() getUserWindowStyleSheet() end)
end)
end)
describe("getCmdLineStyleSheet", function()
local originalMainStyleSheet
setup(function()
originalMainStyleSheet = getCmdLineStyleSheet()
end)
teardown(function()
setCmdLineStyleSheet("main", originalMainStyleSheet)
setCmdLineStyleSheet(cmdLine, "")
end)
it("returns the stylesheet set on a created command line", function()
local css = "color: rgb(12,34,56);"
assert.is_true(setCmdLineStyleSheet(cmdLine, css))
assert.are.equal(css, getCmdLineStyleSheet(cmdLine))
end)
it("round-trips an updated stylesheet", function()
setCmdLineStyleSheet(cmdLine, "color: rgb(1,2,3);")
assert.are.equal("color: rgb(1,2,3);", getCmdLineStyleSheet(cmdLine))
setCmdLineStyleSheet(cmdLine, "color: rgb(4,5,6);")
assert.are.equal("color: rgb(4,5,6);", getCmdLineStyleSheet(cmdLine))
end)
it("defaults to the main command line when given no name or nil", function()
-- the one-argument form of the setter targets "main" as well
local css = "color: rgb(9,9,9);"
assert.is_true(setCmdLineStyleSheet(css))
assert.are.equal(css, getCmdLineStyleSheet())
assert.are.equal(css, getCmdLineStyleSheet(nil))
assert.are.equal(css, getCmdLineStyleSheet("main"))
end)
it("returns nil and a message naming an unknown command line", function()
local unknown = name("wdgNoSuchCmdLine")
local ok, err = getCmdLineStyleSheet(unknown)
assert.is_nil(ok)
assert.are.equal(("command-line name '%s' not found"):format(unknown), err)
end)
end)
describe("getLabelToolTip", function()
teardown(function()
resetLabelToolTip(label)
end)
it("returns the tooltip set by setLabelToolTip", function()
assert.is_true(setLabelToolTip(label, "a tooltip"))
assert.are.equal("a tooltip", getLabelToolTip(label))
end)
-- only the text is read back: the setter's duration reaches Qt's own
-- tooltip timer, which reinterprets it, so it is not part of this getter
it("keeps the text when a display duration is given", function()
assert.is_true(setLabelToolTip(label, "a timed tooltip", 5))
assert.are.equal("a timed tooltip", getLabelToolTip(label))
end)
it("round-trips a multi-byte tooltip unchanged", function()
assert.is_true(setLabelToolTip(label, "Ünïcödé tooltip - 日本語"))
assert.are.equal("Ünïcödé tooltip - 日本語", getLabelToolTip(label))
end)
it("reports an empty tooltip after resetLabelToolTip", function()
setLabelToolTip(label, "a tooltip to clear")
assert.is_true(resetLabelToolTip(label))
assert.are.equal("", getLabelToolTip(label))
end)
it("returns nil and a message naming an unknown label", function()
local unknown = name("wdgNoSuchLabel")
local ok, err = getLabelToolTip(unknown)
assert.is_nil(ok)
assert.are.equal(("label name '%s' not found"):format(unknown), err)
end)
it("rejects an empty name the way setLabelToolTip does", function()
local ok, err = getLabelToolTip("")
assert.is_nil(ok)
assert.are.equal("a label cannot have an empty string as its name", err)
end)
it("errors when called without a label name", function()
assert.has_error(function() getLabelToolTip() end)
end)
end)
describe("getScrollBarVisible", function()
local originalMainScrollBar
local freshConsole = name("wdgFreshConsole")
local bufferName = name("wdgBuffer")
setup(function()
originalMainScrollBar = getScrollBarVisible("main")
end)
teardown(function()
-- restore the shared main window even if a spec above bailed out early
if originalMainScrollBar then
enableScrollBar("main")
else
disableScrollBar("main")
end
showWindow(console)
deleteMiniConsole(freshConsole)
deleteMiniConsole(bufferName)
end)
it("reflects enableScrollBar and disableScrollBar on a miniconsole", function()
enableScrollBar(console)
assert.is_true(getScrollBarVisible(console))
disableScrollBar(console)
assert.is_false(getScrollBarVisible(console))
enableScrollBar(console)
assert.is_true(getScrollBarVisible(console))
end)
it("reports a miniconsole's scroll bar as hidden until it is enabled", function()
createMiniConsole(freshConsole, 10, 10, 200, 100)
assert.is_false(getScrollBarVisible(freshConsole))
enableScrollBar(freshConsole)
assert.is_true(getScrollBarVisible(freshConsole))
end)
it("keeps reporting an enabled scroll bar while the console is hidden", function()
-- the reason this reads back an intent rather than the widget: Mudlet
-- hides the whole console of any profile that is not the front tab
enableScrollBar(console)
hideWindow(console)
assert.is_true(getScrollBarVisible(console))
showWindow(console)
assert.is_true(getScrollBarVisible(console))
end)
it("reports a buffer, which never has a scroll bar, as not having one", function()
createBuffer(bufferName)
assert.is_false(getScrollBarVisible(bufferName))
end)
it("reflects disableScrollBar and enableScrollBar on the main window", function()
disableScrollBar("main")
assert.is_false(getScrollBarVisible("main"))
enableScrollBar("main")
assert.is_true(getScrollBarVisible("main"))
end)
it("defaults to the main window when given no name", function()
disableScrollBar("main")
assert.is_false(getScrollBarVisible())
enableScrollBar("main")
assert.is_true(getScrollBarVisible())
end)
it("returns nil and a message naming an unknown window", function()
local unknown = name("wdgNoSuchWindow")
local ok, err = getScrollBarVisible(unknown)
assert.is_nil(ok)
assert.are.equal(('window "%s" not found'):format(unknown), err)
end)
end)
-- The "no map widget" error path for these two is covered in Mapper_spec,
-- which runs first and whose opening spec is the only point in the session
-- where the widget does not exist yet.
describe("map widget getters", function()
setup(function()
assert.is_true(openMapWidget())
end)
teardown(function()
resetMapWindowTitle()
-- resizeMapWidget/moveMapWidget force the widget floating; put it back so
-- this block does not hand a floating map widget to whatever runs next
openMapWidget("r")
end)
it("getMapWindowTitle returns the title set by setMapWindowTitle", function()
assert.is_true(setMapWindowTitle("A map title"))
assert.are.equal("A map title", getMapWindowTitle())
end)
it("getMapWindowTitle round-trips an updated title", function()
setMapWindowTitle("first map title")
assert.are.equal("first map title", getMapWindowTitle())
setMapWindowTitle("second map title")
assert.are.equal("second map title", getMapWindowTitle())
end)
it("getMapWindowTitle reports the generated default after resetMapWindowTitle", function()
setMapWindowTitle("not the default")
assert.is_true(resetMapWindowTitle())
local title = getMapWindowTitle()
assert.are.equal("string", type(title))
assert.is_truthy(title:find(getProfileName(), 1, true))
end)
-- the sizes below are comfortably above the map widget's minimum size hint
-- so that a resize cannot come back clamped
it("getMapWidgetGeometry reflects resizeMapWidget", function()
-- size() is the exact inverse of the resize() resizeMapWidget makes and
-- does not depend on a window manager honouring a move
resizeMapWidget(640, 480)
local _, _, w, h = getMapWidgetGeometry()
assert.are.same({640, 480}, {w, h})
resizeMapWidget(560, 440)
local _, _, w2, h2 = getMapWidgetGeometry()
assert.are.same({560, 440}, {w2, h2})
end)
it("getMapWidgetGeometry reflects moveMapWidget", function()
resizeMapWidget(600, 460)
moveMapWidget(120, 130)
local x1, y1 = getMapWidgetGeometry()
moveMapWidget(300, 350)
local x2, y2, w, h = getMapWidgetGeometry()
-- a window manager can add a constant frame offset to where a floating
-- dock lands, so the movement is asserted rather than the position
assert.are.same({180, 220}, {x2 - x1, y2 - y1})
assert.are.same({600, 460}, {w, h})
end)
it("getMapWidgetGeometry returns exactly four values", function()
assert.are.equal(4, select("#", getMapWidgetGeometry()))
end)
end)
end)