add: getWindowGeometry(), windowVisible() and getLabelText() functions (#9528)

#### Brief overview of PR changes/additions
- `getWindowGeometry(name)` returns x, y, width, height for any window
element - the exact inverse of moveWindow()/resizeWindow()
- `windowVisible(name)` returns effective visibility (a child in a
hidden user window reports false)
- `getLabelText(name)` returns the text shown on a label
- 25 specs appended to UI_spec.lua

#### Motivation for adding to Mudlet
Long-requested readback symmetry: scripts (and now tests) can finally
query window state they could previously only set.

#### Other info (issues closed, discussion etc)
Part of the Lua API test-coverage program (Wave 0); unlocks ~100
previously untestable functions. Wiki text drafted, to be added to Area
51 once merged.

**Test case:** `createLabel` + `moveWindow`/`resizeWindow`, then
`getWindowGeometry` returns the same values; `hideWindow` flips
`windowVisible` to false; `echo` to a label, `getLabelText` returns it.

Awaiting build/test by a maintainer; squash-merge with:
Assisted-by: Claude:claude-opus-4-8
(Signed-off-by to be added at squash after testing)
This commit is contained in:
Vadim Peretokin 2026-07-30 12:04:54 +02:00 committed by GitHub
parent 1cf8a59ba2
commit 1227bc3778
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 353 additions and 0 deletions

View file

@ -4949,6 +4949,72 @@ std::optional<QString> Host::windowType(const QString& name) const
return {};
}
// Returns the position and size of a named window element, matching what
// moveWindow()/resizeWindow() set. pos()/size() (rather than geometry()) are
// used deliberately: they are the exact inverse of the move()/resize() calls
// those setters make, including for a floating user-window dock where move()
// targets the frame origin while geometry() would report the client area.
// Mirrors the widget dispatch of moveWindow()/resizeWindow(); user windows are
// moved/resized through their dock widget, so read the dock, not the console.
std::optional<QRect> Host::windowGeometry(const QString& name) const
{
if (!mpConsole) {
return {};
}
if (auto pL = mpConsole->mLabelMap.value(name)) {
return {QRect(pL->pos(), pL->size())};
}
if (auto pC = mpConsole->mSubConsoleMap.value(name)) {
if (auto pD = mpConsole->mDockWidgetMap.value(name)) {
return {QRect(pD->pos(), pD->size())};
}
return {QRect(pC->pos(), pC->size())};
}
if (auto pS = mpConsole->mScrollBoxMap.value(name)) {
return {QRect(pS->pos(), pS->size())};
}
if (auto pN = mpConsole->mSubCommandLineMap.value(name)) {
return {QRect(pN->pos(), pN->size())};
}
if (auto pT = mpConsole->mTextBoxMap.value(name)) {
return {QRect(pT->pos(), pT->size())};
}
return {};
}
// Returns whether a named window element is currently visible. Mirrors the
// widget dispatch of hideWindow()/showWindow(); user windows report the
// visibility of their dock widget, which is what those functions toggle.
std::optional<bool> Host::windowVisible(const QString& name) const
{
if (!mpConsole) {
return {};
}
if (auto pL = mpConsole->mLabelMap.value(name)) {
return {pL->isVisible()};
}
if (auto pC = mpConsole->mSubConsoleMap.value(name)) {
if (auto pD = mpConsole->mDockWidgetMap.value(name)) {
return {pD->isVisible()};
}
return {pC->isVisible()};
}
if (auto pS = mpConsole->mScrollBoxMap.value(name)) {
return {pS->isVisible()};
}
if (auto pN = mpConsole->mSubCommandLineMap.value(name)) {
return {pN->isVisible()};
}
if (auto pT = mpConsole->mTextBoxMap.value(name)) {
return {pT->isVisible()};
}
return {};
}
void Host::setLargeAreaExitArrows(const bool state)
{
if (mLargeAreaExitArrows != state) {

View file

@ -47,6 +47,7 @@
#include <QList>
#include <QMargins>
#include <QPointer>
#include <QRect>
#include <QStack>
#include <QTextStream>
@ -466,6 +467,8 @@ public:
mScreenHeight = height;
}
std::optional<QString> windowType(const QString& name) const;
std::optional<QRect> windowGeometry(const QString& name) const;
std::optional<bool> windowVisible(const QString& name) const;
bool getEditorShowBidi() const { return mEditorShowBidi; }
void setEditorShowBidi(const bool);
bool caretEnabled() const;

View file

@ -5289,6 +5289,9 @@ void TLuaInterpreter::initLuaGlobals()
lua_register(pGlobalLua, "setTextFormat", TLuaInterpreter::setTextFormat);
lua_register(pGlobalLua, "getMainWindowSize", TLuaInterpreter::getMainWindowSize);
lua_register(pGlobalLua, "getUserWindowSize", TLuaInterpreter::getUserWindowSize);
lua_register(pGlobalLua, "getWindowGeometry", TLuaInterpreter::getWindowGeometry);
lua_register(pGlobalLua, "windowVisible", TLuaInterpreter::windowVisible);
lua_register(pGlobalLua, "getLabelText", TLuaInterpreter::getLabelText);
lua_register(pGlobalLua, "getMousePosition", TLuaInterpreter::getMousePosition);
lua_register(pGlobalLua, "setProfileIcon", TLuaInterpreter::setProfileIcon);
lua_register(pGlobalLua, "resetProfileIcon", TLuaInterpreter::resetProfileIcon);

View file

@ -495,6 +495,9 @@ public:
static int setLabelOnLeave(lua_State*);
static int getMainWindowSize(lua_State*);
static int getUserWindowSize(lua_State*);
static int getWindowGeometry(lua_State*);
static int windowVisible(lua_State*);
static int getLabelText(lua_State*);
static int getMousePosition(lua_State*);
static int setProfileIcon(lua_State*);
static int resetProfileIcon(lua_State*);

View file

@ -1675,6 +1675,50 @@ int TLuaInterpreter::getUserWindowSize(lua_State* L)
return 2;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#getWindowGeometry
int TLuaInterpreter::getWindowGeometry(lua_State* L)
{
const Host& host = getHostFromLua(L);
const QString windowName = getVerifiedString(L, __func__, 1, "window name");
if (auto geometry = host.windowGeometry(windowName)) {
lua_pushnumber(L, geometry->x());
lua_pushnumber(L, geometry->y());
lua_pushnumber(L, geometry->width());
lua_pushnumber(L, geometry->height());
return 4;
}
lua_pushnil(L);
lua_pushfstring(L, bad_window_value, windowName.toUtf8().constData());
return 2;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#windowVisible
int TLuaInterpreter::windowVisible(lua_State* L)
{
const Host& host = getHostFromLua(L);
const QString windowName = getVerifiedString(L, __func__, 1, "window name");
if (auto visible = host.windowVisible(windowName)) {
lua_pushboolean(L, *visible);
return 1;
}
lua_pushnil(L);
lua_pushfstring(L, bad_window_value, windowName.toUtf8().constData());
return 2;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#getLabelText
int TLuaInterpreter::getLabelText(lua_State* L)
{
const QString labelName = getVerifiedString(L, __func__, 1, "label name");
auto label = LABEL(L, labelName);
lua_pushstring(L, label->text().toUtf8().constData());
return 1;
}
// Documentation: https://wiki.mudlet.org/w/Manual:Lua_Functions#getWindowWrap
int TLuaInterpreter::getWindowWrap(lua_State* L)
{

View file

@ -2524,3 +2524,237 @@ describe("Tests UI functions", function()
end)
end)
end)
-- Window state getters: getWindowGeometry, windowVisible, getLabelText.
-- Self-contained top-level block kept at the tail of the file; do not
-- interleave it with the "Tests UI functions" block above.
describe("Window state getters", function()
-- Unique-ish names so repeat runs against the same profile do not collide:
-- user windows cannot be deleted from Lua, only hidden.
local suffix = ("-%d-%d"):format(os.time(), math.random(100000))
local labelName = "wsgLabel" .. suffix
local consoleName = "wsgConsole" .. suffix
local scrollBoxName = "wsgScrollBox" .. suffix
local cmdLineName = "wsgCmdLine" .. suffix
local textEditName = "wsgTextEdit" .. suffix
local userWindowName = "wsgUserWindow" .. suffix
-- a label parented inside the user window, to probe ancestor-aware visibility
local childLabelName = "wsgChildLabel" .. suffix
setup(function()
createLabel(labelName, 10, 20, 100, 50, 1)
createMiniConsole(consoleName, 30, 40, 300, 150)
createScrollBox(scrollBoxName, 60, 70, 120, 90)
createCommandLine(cmdLineName, 15, 25, 140, 35)
createTextEdit(textEditName, 45, 55, 160, 110)
openUserWindow(userWindowName)
createLabel(userWindowName, childLabelName, 5, 5, 40, 20, 1)
end)
before_each(function()
-- restore baseline geometry and visibility so one failing spec cannot
-- cascade into later specs (busted runs specs in definition order)
moveWindow(labelName, 10, 20)
resizeWindow(labelName, 100, 50)
moveWindow(consoleName, 30, 40)
resizeWindow(consoleName, 300, 150)
for _, name in ipairs({labelName, consoleName, scrollBoxName, cmdLineName, textEditName, userWindowName}) do
showWindow(name)
end
end)
teardown(function()
deleteLabel(childLabelName)
deleteLabel(labelName)
deleteMiniConsole(consoleName)
deleteScrollBox(scrollBoxName)
deleteCommandLine(cmdLineName)
deleteTextEdit(textEditName)
-- user windows cannot be deleted from Lua, so just hide it again
hideWindow(userWindowName)
end)
describe("getWindowGeometry", function()
it("returns a label's position and size as x, y, width, height", function()
local x, y, w, h = getWindowGeometry(labelName)
assert.are.equal(10, x)
assert.are.equal(20, y)
assert.are.equal(100, w)
assert.are.equal(50, h)
end)
it("returns a miniconsole's position and size", function()
local x, y, w, h = getWindowGeometry(consoleName)
assert.are.equal(30, x)
assert.are.equal(40, y)
assert.are.equal(300, w)
assert.are.equal(150, h)
end)
it("returns a scroll box's position and size", function()
local x, y, w, h = getWindowGeometry(scrollBoxName)
assert.are.equal(60, x)
assert.are.equal(70, y)
assert.are.equal(120, w)
assert.are.equal(90, h)
end)
it("returns a command line's position and size", function()
local x, y, w, h = getWindowGeometry(cmdLineName)
assert.are.equal(15, x)
assert.are.equal(25, y)
assert.are.equal(140, w)
assert.are.equal(35, h)
end)
it("returns a text edit's position and size", function()
local x, y, w, h = getWindowGeometry(textEditName)
assert.are.equal(45, x)
assert.are.equal(55, y)
assert.are.equal(160, w)
assert.are.equal(110, h)
end)
it("reflects moveWindow on a label", function()
moveWindow(labelName, 55, 66)
local x, y = getWindowGeometry(labelName)
assert.are.equal(55, x)
assert.are.equal(66, y)
end)
it("reflects resizeWindow on a miniconsole", function()
resizeWindow(consoleName, 321, 123)
local _, _, w, h = getWindowGeometry(consoleName)
assert.are.equal(321, w)
assert.are.equal(123, h)
end)
it("reflects resizeWindow on a user window", function()
-- read back through the dock widget; size() is the exact inverse of
-- resize() and does not depend on the window manager honouring a move
resizeWindow(userWindowName, 400, 200)
local _, _, w, h = getWindowGeometry(userWindowName)
assert.are.equal(400, w)
assert.are.equal(200, h)
end)
it("returns nil and a message naming an unknown window", function()
local result, err = getWindowGeometry("wsgNoSuchWindow")
assert.is_nil(result)
assert.are.equal("string", type(err))
assert.is_truthy(err:find("wsgNoSuchWindow", 1, true))
end)
it("returns nil and a message for the main window", function()
-- mirrors moveWindow/resizeWindow, which likewise do not act on "main"
local result, err = getWindowGeometry("main")
assert.is_nil(result)
assert.are.equal("string", type(err))
end)
it("errors when called without a window name", function()
assert.has_error(function() getWindowGeometry() end)
end)
end)
describe("windowVisible", function()
it("reflects hideWindow then showWindow on a label", function()
assert.is_true(windowVisible(labelName))
hideWindow(labelName)
assert.is_false(windowVisible(labelName))
showWindow(labelName)
assert.is_true(windowVisible(labelName))
end)
it("reflects hideWindow then showWindow on a miniconsole", function()
assert.is_true(windowVisible(consoleName))
hideWindow(consoleName)
assert.is_false(windowVisible(consoleName))
showWindow(consoleName)
assert.is_true(windowVisible(consoleName))
end)
it("reflects hideWindow then showWindow on a scroll box", function()
assert.is_true(windowVisible(scrollBoxName))
hideWindow(scrollBoxName)
assert.is_false(windowVisible(scrollBoxName))
showWindow(scrollBoxName)
assert.is_true(windowVisible(scrollBoxName))
end)
it("reflects hideWindow then showWindow on a command line", function()
assert.is_true(windowVisible(cmdLineName))
hideWindow(cmdLineName)
assert.is_false(windowVisible(cmdLineName))
showWindow(cmdLineName)
assert.is_true(windowVisible(cmdLineName))
end)
it("reflects hideWindow then showWindow on a user window", function()
assert.is_true(windowVisible(userWindowName))
hideWindow(userWindowName)
assert.is_false(windowVisible(userWindowName))
showWindow(userWindowName)
assert.is_true(windowVisible(userWindowName))
end)
it("reports a child hidden by its user window as not visible", function()
-- windowVisible reflects effective (ancestor-aware) visibility: hiding
-- the parent user window hides the child even though the child itself
-- was never hidden
assert.is_true(windowVisible(childLabelName))
hideWindow(userWindowName)
assert.is_false(windowVisible(childLabelName))
showWindow(userWindowName)
assert.is_true(windowVisible(childLabelName))
end)
it("returns nil and a message naming an unknown window", function()
local result, err = windowVisible("wsgNoSuchWindow")
assert.is_nil(result)
assert.are.equal("string", type(err))
assert.is_truthy(err:find("wsgNoSuchWindow", 1, true))
end)
it("returns nil and a message for the main window", function()
local result, err = windowVisible("main")
assert.is_nil(result)
assert.are.equal("string", type(err))
end)
it("errors when called without a window name", function()
assert.has_error(function() windowVisible() end)
end)
end)
describe("getLabelText", function()
it("returns text set on a label via echo", function()
echo(labelName, "hello label")
assert.are.equal("hello label", getLabelText(labelName))
end)
it("round-trips updated label text", function()
echo(labelName, "first")
assert.are.equal("first", getLabelText(labelName))
echo(labelName, "second")
assert.are.equal("second", getLabelText(labelName))
end)
it("returns nil and a message naming an unknown label", function()
local result, err = getLabelText("wsgNoSuchLabel")
assert.is_nil(result)
assert.are.equal("string", type(err))
assert.is_truthy(err:find("wsgNoSuchLabel", 1, true))
end)
it("returns nil and a message for a non-label window", function()
local result, err = getLabelText(consoleName)
assert.is_nil(result)
assert.are.equal("string", type(err))
end)
it("errors when called without a label name", function()
assert.has_error(function() getLabelText() end)
end)
end)
end)