From 8da2f20a9f061b83c091f162341c1063243b5ff5 Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 27 Oct 2026 08:15:44 -0400 Subject: [PATCH 1/8] llList2Key's key made text holds its item's text, not NULL_KEY's With keys as uuids, a key held as text (declared with nothing, or given "") that was given llList2Key's item took tostring of SLua's List2Key, which answers NULL_KEY for "" and for any text that is no UUID: key av; av = llList2Key(["", ...], 0); av != "" was true, where LSL's was false. Wherever llList2Key's key is made text (Writer::coerced), its item is read as List2String reads it, as it already was with keys as text: av = newAssignments[1] or "". A key held as a uuid keeps List2Key. Co-Authored-By: Claude Opus 5.5 --- indra/alscript/lsl/allsltoslua.cpp | 36 ++++++++++----- indra/alscript/tests/allsltoslua_test.cpp | 56 +++++++++++++++++++++-- 2 files changed, 78 insertions(+), 14 deletions(-) diff --git a/indra/alscript/lsl/allsltoslua.cpp b/indra/alscript/lsl/allsltoslua.cpp index 81a0bae247..a517627272 100644 --- a/indra/alscript/lsl/allsltoslua.cpp +++ b/indra/alscript/lsl/allsltoslua.cpp @@ -992,12 +992,13 @@ namespace // Whether listItem writes a call as an index into its list: its // items already what is asked for. bool listItemFits(LSLFunctionExpression* e); - // llList2Key where keys are held as text: its item read as text, as - // LSL gave it -- a string's, a key's -- where SLua's List2Key - // answers NULL_KEY for text that is no UUID. + // llList2Key where keys are held as text, or where what it gives is + // made text (Writer::coerced): its item read as text, as LSL gave + // it -- a string's, a key's -- where SLua's List2Key answers + // NULL_KEY for text that is no UUID. bool textKeyItem(LSLFunctionExpression* e) const { - return !mOptions.uuidKeys && std::string_view(e->getIdentifier()->getName()) == "llList2Key"; + return (!mOptions.uuidKeys || mTextItems.contains(e)) && std::string_view(e->getIdentifier()->getName()) == "llList2Key"; } // --- keys that hold text ----------------------------------------------------- @@ -1186,6 +1187,8 @@ namespace // Comparisons written the other way round, a not before them. boost::unordered_flat_set mFlipped; boost::unordered_flat_set mTextKeys; + // llList2Key's calls whose key is made text (textKeyItem). + boost::unordered_flat_set mTextItems; }; std::string Writer::said(const char* key, const char* english, const std::vector& args) const @@ -2764,6 +2767,13 @@ namespace Expr Writer::coerced(LSLExpression* e, LSLIType to) { + // llList2Key's key made text: its item read as text instead, as LSL + // gave it, where the text of SLua's List2Key of "" is NULL_KEY's. + if (LSLExpression* inner = unbracketed(e); to == LST_STRING && inner && inner->getNodeSubType() == NODE_FUNCTION_EXPRESSION && + std::string_view(static_cast(inner)->getIdentifier()->getName()) == "llList2Key") + { + mTextItems.insert(inner); + } Expr out = value(e); const LSLIType from = slType(e); if (to == LST_KEY && from == LST_STRING) @@ -3852,7 +3862,7 @@ namespace return *written; } } - // llList2Key's item as text, where keys are held as text: as + // llList2Key's item as text, where its key is made text: as // List2String reads it, which differs only for an item neither a // string nor a key. if (textKeyItem(e)) @@ -3860,7 +3870,7 @@ namespace if ((itemTypes(argumentAt(e, 0)) & ~(ItemString | ItemKey)) != 0) { noteOnce(e, "SluaList2KeyText", - "Keys are held as text, so llList2Key's item is read as List2String reads it, as LSL gave it, where SLua's " + "Its key is made text, so llList2Key's item is read as List2String reads it, as LSL gave it, where SLua's " "List2Key answers NULL_KEY for text that is no UUID; a number's or a vector's text is given, where LSL gave \"\"."); } lsl = "llList2String"; @@ -7699,12 +7709,16 @@ namespace return LST_KEY; } LSLExpression* inner = unbracketed(e); - // Where the script's keys are held as text: a cast to a key, a call - // of its own that gives one, and llList2Key's item (textKeyItem). + // Where the script's keys are held as text: a cast to a key, and a + // call of its own that gives one. if (!mOptions.uuidKeys && inner && e->getIType() == LST_KEY && - (inner->getNodeSubType() == NODE_TYPECAST_EXPRESSION || - (inner->getNodeSubType() == NODE_FUNCTION_EXPRESSION && - (readOf(inner) || textKeyItem(static_cast(inner)))))) + (inner->getNodeSubType() == NODE_TYPECAST_EXPRESSION || (inner->getNodeSubType() == NODE_FUNCTION_EXPRESSION && readOf(inner)))) + { + return LST_STRING; + } + // llList2Key's item, read as text (textKeyItem). + if (inner && e->getIType() == LST_KEY && inner->getNodeSubType() == NODE_FUNCTION_EXPRESSION && + textKeyItem(static_cast(inner))) { return LST_STRING; } diff --git a/indra/alscript/tests/allsltoslua_test.cpp b/indra/alscript/tests/allsltoslua_test.cpp index 50eeac7a68..f5fc7cfb23 100644 --- a/indra/alscript/tests/allsltoslua_test.cpp +++ b/indra/alscript/tests/allsltoslua_test.cpp @@ -4946,10 +4946,15 @@ namespace const ALLSLToSLua::Result uuids = require_converted(lsl); CAPTURE(uuids.text); { - INFO("uuids by default: List2Key's uuid"); - CHECK(has(uuids, "local msg = tostring(llcompat.List2Key(l, 0))")); + INFO("uuids by default: List2Key's uuid for a key"); CHECK(has(uuids, "local k = llcompat.List2Key(l, 0)")); - CHECK_FALSE(noted(uuids, "SluaList2KeyText")); + } + { + INFO("and its item's text where it is made text, which tostring of List2Key's NULL_KEY was not (QJ1)"); + CHECK(has(uuids, "local msg = l[1] or \"\"\n")); + CHECK(has(uuids, "print(llcompat.List2String(gMixed, 0))")); + CHECK(noted(uuids, "SluaList2KeyText")); + CHECK_FALSE(has(uuids, "tostring(llcompat.List2Key(")); } } @@ -5120,4 +5125,49 @@ namespace CHECK(has(r, "print(tostring(_))\n")); require_checks_clean(r); } + + + // Tapple's: a key held as text given llList2Key of a list of "" took + // tostring of SLua's List2Key, whose "" is NULL_KEY, so it was no + // longer "" and went into the list. + TEST_CASE_METHOD(allsltoslua_data, "ALLSLToSLua: a key held as text given llList2Key's item holds the item's text", "[ALLSLToSLua]") + { + const std::string body = " list newlyStood = [];\n" + " list newAssignments = [\"\", \"\", \"\", \"\"];\n" + " av = llList2Key(newAssignments, 0);\n" + " if (av != \"\") {\n" + " newlyStood += [av];\n" + " }\n" + " llOwnerSay(llList2Json(JSON_ARRAY, newlyStood));\n" + " seated(llList2Key(newAssignments, 1));\n" + "} }\n"; + const std::string seated = "seated(key who) { if (who == \"\") llOwnerSay(\"empty\"); }\n"; + ALLSLToSLua::Options o; + o.types = true; + for (const char* decl : { "key av;\n", "key av = \"\";\n" }) + { + const ALLSLToSLua::Result r = ALLSLToSLua::convert(seated + "default { state_entry() {\n " + decl + body, o); + CAPTURE(decl, r.text); + REQUIRE(r.converted); + CHECK(has(r, "local av: string = \"\"\n")); + CHECK(has(r, "\nav = newAssignments[1] or \"\"\n")); + CHECK(has(r, "\nif av ~= \"\" then\n")); + UNSCOPED_INFO("a parameter held as a uuid keeps List2Key's uuid, which equals \"\" as LSL's did"); + CHECK(has(r, "local function seated(who: uuid)")); + CHECK(has(r, "seated(llcompat.List2Key(newAssignments, 1))")); + CHECK(has(r, "if who == uuid(\"\") then")); + CHECK_FALSE(has(r, "tostring(llcompat.List2Key(")); + require_checks_clean(r); + } + { + INFO("a uuid key keeps List2Key, as Tapple asked"); + const ALLSLToSLua::Result r = ALLSLToSLua::convert("default { state_entry() {\n key av = NULL_KEY;\n" + body.substr(0, body.find(" seated")) + "} }\n", o); + CAPTURE(r.text); + REQUIRE(r.converted); + CHECK(has(r, "local av: uuid = NULL_KEY\n")); + CHECK(has(r, "\nav = llcompat.List2Key(newAssignments, 0)\n")); + CHECK(has(r, "\nif av ~= uuid(\"\") then\n")); + require_checks_clean(r); + } + } } From 6edb78248eec61ae1525dd8cb0e922a96a2b844c Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 27 Oct 2026 08:19:32 -0400 Subject: [PATCH 2/8] A file's tab keeps the prims of the scripts it masters in the explorer The explorer lists the objects pinned, selected in world, or holding an open script, and a file's tab names no prim: a prim whose open scripts were all linked to files, their tabs giving way to the files', dropped out once the edit window closed and deselected it. A file's tab now counts as open for every script it masters (not orphaned), and links made and let go of relist the explorer. Co-Authored-By: Claude Opus 5.5 --- indra/newview/alfloaterscriptstudio.cpp | 6 ++++-- indra/newview/alscriptexplorerpane.cpp | 12 ++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/indra/newview/alfloaterscriptstudio.cpp b/indra/newview/alfloaterscriptstudio.cpp index 1e6c4f4636..f9acff3e73 100644 --- a/indra/newview/alfloaterscriptstudio.cpp +++ b/indra/newview/alfloaterscriptstudio.cpp @@ -896,11 +896,13 @@ void ALFloaterScriptStudio::listenToWorkspace() // A file's tab says how many scripts it is the master of, which links // made and let go of change. mLinkBadgesConnection = ALScriptLinkBadges::instance().onChanged([this]() { fillTabs(); }); - // A file's tab stands in for the one script it masters, which links - // made and let go of change. + // A file's tab stands in for the one script it masters, and keeps the + // prims of every script it masters listed, which links made and let go + // of change. mMastersConnection = ALScriptDiskMasters::instance().onChanged([this]() { restandAll(); refreshToolbar(); + relistExplorer(); }); // New definitions from the region: the analyzers reload, the words // are rebuilt, and every script is checked again. diff --git a/indra/newview/alscriptexplorerpane.cpp b/indra/newview/alscriptexplorerpane.cpp index 7c99db0d21..807070d89e 100644 --- a/indra/newview/alscriptexplorerpane.cpp +++ b/indra/newview/alscriptexplorerpane.cpp @@ -442,12 +442,24 @@ void ALScriptExplorerPane::relist(bool refetch, bool from_region) }; listing.nameless = [this](const LLUUID& root) { return nameGivenTo(root); }; listing.selected = mRoots; + // A prim holding a script open, or open through its file: a file's tab + // is open for each script it masters, which has no tab of its own. for (const ALScriptStudioDoc* doc : mServices->openDocs()) { if (!doc->ref.inInventory()) { listing.open.push_back(doc->ref.object); } + else if (!doc->file.empty()) + { + for (const ALMasterLink& link : ALScriptDiskMasters::instance().mastering(doc->file)) + { + if (link.state != ALMasterLink::State::Orphaned && link.object.notNull()) + { + listing.open.push_back(link.object); + } + } + } } listing.unnamed = mServices->words("ObjectUnnamed"); mModel.list(listing); From d3ea7cb2486d71e33e8bb99d2c6dc9eae4d577fd Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 27 Oct 2026 08:21:51 -0400 Subject: [PATCH 3/8] Hover on an SLua event's handler says its parameters again function LLEvents.run_time_permissions() showed nothing under the old solver, which types only LLEvents in a function statement's name, and the editor's fallback asked the definitions for the dotted name, which they keep events under bare. The old editor showed the event's arguments. A field with no type of its own now takes the one the table or extern type it is on gives it, and a field holding a function or nil, as an event's handler does, hovers as the function: function LLEvents.run_time_permissions(perm: number), under either solver. Co-Authored-By: Claude Opus 5.5 --- indra/alscript/luau/alluauservice.cpp | 61 +++++++++++++++++++++ indra/alscript/tests/alluauservice_test.cpp | 27 +++++++++ 2 files changed, 88 insertions(+) diff --git a/indra/alscript/luau/alluauservice.cpp b/indra/alscript/luau/alluauservice.cpp index 450230948c..2a6e3c531c 100644 --- a/indra/alscript/luau/alluauservice.cpp +++ b/indra/alscript/luau/alluauservice.cpp @@ -1831,6 +1831,31 @@ namespace } return true; } + + // A field's type as the table or extern type it is on has it, where the + // field itself was given none. + std::optional fieldTypeOf(const Luau::Module& module, const Luau::AstExprIndexName& field) + { + const Luau::TypeId* table = module.astTypes.find(field.expr); + if (!table) + { + return std::nullopt; + } + const Luau::TypeId type = Luau::follow(*table); + if (const Luau::TableType* t = Luau::getTableType(type)) + { + const auto it = t->props.find(field.index.value); + return it == t->props.end() ? std::nullopt : it->second.readTy; + } + if (const Luau::ExternType* cls = Luau::get(type)) + { + if (const Luau::Property* prop = Luau::lookupExternTypeProp(cls, field.index.value)) + { + return prop->readTy; + } + } + return std::nullopt; + } } ALScriptHover ALLuauService::hover(std::string_view source, S32 line, S32 column) @@ -1878,6 +1903,42 @@ ALScriptHover ALLuauService::hover(std::string_view source, S32 line, S32 column } } } + // A field a function statement gives a function -- `function + // LLEvents.on_rez(n)` -- which the old solver types only the table of: + // the field as the table has it. And a field that holds a function or + // nil, as an event's handler does, by its function: what is written + // there, and what is said of it. + if (Luau::AstExprIndexName* field = expr ? expr->as() : nullptr) + { + if (!type) + { + type = fieldTypeOf(*module, *field); + } + if (type) + { + if (const Luau::UnionType* either = Luau::get(Luau::follow(*type))) + { + std::optional function; + bool other = false; + for (Luau::TypeId option : either->options) + { + const Luau::TypeId one = Luau::follow(option); + if (Luau::get(one) && !function) + { + function = one; + } + else if (!Luau::isNil(one)) + { + other = true; + } + } + if (function && !other) + { + type = function; + } + } + } + } if (!type) { return answer; diff --git a/indra/alscript/tests/alluauservice_test.cpp b/indra/alscript/tests/alluauservice_test.cpp index c4a39a3902..b8f1170838 100644 --- a/indra/alscript/tests/alluauservice_test.cpp +++ b/indra/alscript/tests/alluauservice_test.cpp @@ -657,6 +657,33 @@ namespace CHECK(hover.label.rfind(newSolver ? "function print<" : "function print(", 0) == 0); } + // Tapple's: an event's handler, as it is written, tells its parameters + // under either solver; the old one typed only LLEvents there. + TEST_CASE_METHOD(alluauservice_data, "alluauservice: hover on an event's handler being written gives its parameters", "[alluauservice]") + { + UNSCOPED_INFO("definitions loaded: " + error); + CHECK(loaded); + const std::string script = "function LLEvents.run_time_permissions()\n" + "end\n" + "print(LLEvents.touch_start)\n"; + for (const bool solver : { newSolver, !newSolver }) + { + std::string why; + REQUIRE(service.setNewSolver(solver, why)); + CAPTURE(solver); + ALScriptHover hover = service.hover(script, 0, 20); // run_time_permissions + REQUIRE(hover.found); + UNSCOPED_INFO("the handler by its signature: " + hover.label); + CHECK(hover.label.rfind("function LLEvents.run_time_permissions(perm: number)", 0) == 0); + hover = service.hover(script, 2, 18); // touch_start, read + REQUIRE(hover.found); + UNSCOPED_INFO("and read: " + hover.label); + CHECK(hover.label.find("detected: {DetectedEvent}") != std::string::npos); + } + std::string why; + CHECK(service.setNewSolver(newSolver, why)); + } + TEST_CASE_METHOD(alluauservice_data, "alluauservice: the configuration adds globals and turns lints off until it is replaced", "[alluauservice]") { UNSCOPED_INFO("definitions loaded: " + error); From 84d66654d6fe2eefc2ee2ae59b12361a97a4eddc Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 27 Oct 2026 08:24:46 -0400 Subject: [PATCH 4/8] Compare with the world's script says why it could not read it The Compare a link offers read the script in-world and, where the read failed, returned without a word, so the button only closed the notice. The failure is now said on the file's tab, with the reason the read gave (MasterWorldNotRead). The masters test now takes Compare through a refused read and a read one. Co-Authored-By: Claude Opus 5.5 --- indra/newview/alscriptstudiomasters.cpp | 11 +++++- .../default/xui/en/floater_script_studio.xml | 1 + .../tests/alscriptstudiomasters_test.cpp | 38 +++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/indra/newview/alscriptstudiomasters.cpp b/indra/newview/alscriptstudiomasters.cpp index c7816fa74e..7dcb4a2235 100644 --- a/indra/newview/alscriptstudiomasters.cpp +++ b/indra/newview/alscriptstudiomasters.cpp @@ -574,10 +574,19 @@ void ALScriptStudioMasters::compareWithWorld(Doc& doc, const ALScriptRef& ref) const std::weak_ptr alive = mAlive; mWindow.loadWorldText(ref, [this, alive, id](const ALScriptLoaded& loaded) { Doc* found = alive.lock() ? mServices.findDoc(id) : nullptr; - if (!found || !loaded.error.empty()) + if (!found) { return; } + // Not read: said, rather than the button having done nothing. + if (!loaded.error.empty()) + { + LLStringUtil::format_map_t args; + args["[NAME]"] = loaded.name.empty() ? found->name : loaded.name; + args["[WHY]"] = loaded.error; + mServices.report(mServices.words("MasterWorldNotRead", args), true, found); + return; + } // The author's text as it is in the world, out of its envelope. std::string theirs = loaded.text; if (const std::optional envelope = ALScriptEnvelope::parse(loaded.text)) diff --git a/indra/newview/skins/default/xui/en/floater_script_studio.xml b/indra/newview/skins/default/xui/en/floater_script_studio.xml index b46dfae4e7..7d95133122 100644 --- a/indra/newview/skins/default/xui/en/floater_script_studio.xml +++ b/indra/newview/skins/default/xui/en/floater_script_studio.xml @@ -673,6 +673,7 @@ A notecard dropped on the vimrc box in Script Studio Preferences (Editor tab) is From [NAME]'s next save, its script runs under [EXPERIENCE]. From [NAME]'s next save, its script runs under no experience. [FILE] could not be read to compare with [NAME]. + [NAME] could not be read in-world to compare: [WHY] Asking what [OBJECTS] holds... Reading the scripts of [OBJECTS]: [DONE] of [COUNT]... Looking on disk for the file of 1 script... diff --git a/indra/newview/tests/alscriptstudiomasters_test.cpp b/indra/newview/tests/alscriptstudiomasters_test.cpp index a5b706fa2c..119fb84d22 100644 --- a/indra/newview/tests/alscriptstudiomasters_test.cpp +++ b/indra/newview/tests/alscriptstudiomasters_test.cpp @@ -552,6 +552,44 @@ namespace CHECK(said().doc == lamp->id); CHECK(said().actions == Names{ "master_send", "master_compare" }); } + + // Tapple's: Compare did nothing but close the notice. What the world holds, read, is + // compared; where it cannot be read, said. + const size_t loads = studio->loads.size(); + const size_t compared = studio->compared.size(); + unit->offer(*lamp, "master_compare"); + { + INFO("the script in-world asked for"); + REQUIRE(studio->loads.size() == loads + 1); + CHECK(studio->loads.back() == other_ref); + REQUIRE(studio->toLoad); + } + ALScriptLoaded refused; + refused.ref = other_ref; + refused.name = "lamp"; + refused.error = "no such item"; + studio->toLoad(refused); + { + INFO("not read: said on the file's tab, nothing compared"); + CHECK(studio->compared.size() == compared); + CHECK(says(said().text, "MasterWorldNotRead")); + CHECK(said().text.find("no such item") != std::string::npos); + CHECK(said().failure); + CHECK(said().doc == lamp->id); + } + unit->offer(*lamp, "master_compare"); + ALScriptLoaded read; + read.ref = other_ref; + read.name = "lamp"; + read.text = "default { state_entry() {} }\n"; + studio->toLoad(read); + { + INFO("read: the file's tab beside what the world holds"); + REQUIRE(studio->compared.size() == compared + 1); + CHECK(studio->compared.back().doc == lamp->id); + CHECK(studio->compared.back().left == "default {}\n"); + CHECK(studio->compared.back().right == "default { state_entry() {} }\n"); + } } // A tab with unsaved changes linked is asked first: Cancel links nothing, Discard holds the From 44674fb4ea2f4845e16d56f6eb5a0c71856ba678 Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 27 Oct 2026 08:31:16 -0400 Subject: [PATCH 5/8] Script Studio's notice wraps its words onto up to three lines A link's notice ran three times past the one line the bar had, cut with an ellipsis and whole only in its tip. The bar's words now wrap at what its buttons leave them, and its layout panel is as tall as their lines, up to three, again as the window is resized; all of them are still in the tip. The link's own words are shorter too: "[NAME] is linked to [FILE]: each save of the file sends it. They differ now: the next save sends the file, keeping what [NAME] held in History." Co-Authored-By: Claude Opus 5.5 --- indra/newview/alscriptnoticebar.cpp | 37 ++++++++++++ indra/newview/alscriptnoticebar.h | 12 +++- .../default/xui/en/floater_script_studio.xml | 8 +-- .../tests/alscriptstudioorphans_test.cpp | 58 +++++++++++++++++++ 4 files changed, 109 insertions(+), 6 deletions(-) diff --git a/indra/newview/alscriptnoticebar.cpp b/indra/newview/alscriptnoticebar.cpp index e805813549..d03d5c6caf 100644 --- a/indra/newview/alscriptnoticebar.cpp +++ b/indra/newview/alscriptnoticebar.cpp @@ -31,6 +31,7 @@ #include "llbutton.h" #include "llfloater.h" #include "llfontgl.h" +#include "lllayoutstack.h" #include "lltextbox.h" static LLPanelInjector t_script_studio_notice("script_studio_notice"); @@ -98,4 +99,40 @@ void ALScriptNoticeBar::show(const Notice& notice) } const LLRect words = mText->getRect(); mText->setShape(LLRect(words.mLeft, words.mTop, llmax(words.mLeft + 40, right - 6), words.mBottom)); + fitWords(); +} + +void ALScriptNoticeBar::reshape(S32 width, S32 height, bool called_from_parent) +{ + const bool resized = width != getRect().getWidth(); + LLPanel::reshape(width, height, called_from_parent); + // The words wrap again at the width they follow. + if (resized && mText && getParent() && getParent()->getVisible()) + { + fitWords(); + } +} + +void ALScriptNoticeBar::fitWords() +{ + // As many lines as the words take at their width, up to MAX_LINES; the + // rest is in the tip. + const LLRect words = mText->getRect(); + const S32 full = mText->getTextPixelHeight(); + const S32 lines = llmax(1, mText->getLineCount()); + const S32 tall = llmax(1, full / lines) * llmin(lines, MAX_LINES); + if (tall != words.getHeight()) + { + mText->setShape(LLRect(words.mLeft, words.mTop, words.mRight, words.mTop - tall)); + } + // The bar as far above the words as below them. + if (LLLayoutPanel* holder = ALViewType::as(getParent())) + { + const S32 margin = getRect().getHeight() - words.mTop; + const S32 wanted = llmax(MIN_HEIGHT, margin + tall + margin); + if (wanted != holder->getTargetDim()) + { + holder->setTargetDim(wanted); + } + } } diff --git a/indra/newview/alscriptnoticebar.h b/indra/newview/alscriptnoticebar.h index 2db768b478..60fa3638cf 100644 --- a/indra/newview/alscriptnoticebar.h +++ b/indra/newview/alscriptnoticebar.h @@ -37,8 +37,9 @@ class LLTextBox; // front has to reckon with -- a text kept from an earlier session, its // object out of sight, its item gone, the connection lost, the file gone // from disk -- with up to four things to be done about it and a way to hide -// it. Its layout panel is shown while it has something to say. What it -// says is ALScriptStudioOrphans's; what its buttons do is the window's. +// it. Its layout panel is shown while it has something to say, as tall as +// its words wrapped, up to three lines. What it says is +// ALScriptStudioOrphans's; what its buttons do is the window's. class ALScriptNoticeBar : public LLPanel { public: @@ -68,11 +69,18 @@ class ALScriptNoticeBar : public LLPanel }; bool postBuild() override; + void reshape(S32 width, S32 height, bool called_from_parent = true) override; void show(const Notice& notice); // What each button does now, from the first. const std::string& action(size_t which) const { return mActions[which < BUTTONS ? which : 0]; } private: + // The words' lines, and the bar's layout panel as tall as they are. + void fitWords(); + + static constexpr S32 MAX_LINES = 3; + static constexpr S32 MIN_HEIGHT = 28; + ALScriptStudioServices* mServices = nullptr; Window* mWindow = nullptr; LLTextBox* mText = nullptr; diff --git a/indra/newview/skins/default/xui/en/floater_script_studio.xml b/indra/newview/skins/default/xui/en/floater_script_studio.xml index 7d95133122..0f1a5fcab4 100644 --- a/indra/newview/skins/default/xui/en/floater_script_studio.xml +++ b/indra/newview/skins/default/xui/en/floater_script_studio.xml @@ -643,9 +643,9 @@ A notecard dropped on the vimrc box in Script Studio Preferences (Editor tab) is [FILE] is not an LSL file, and [NAME] is LSL. [FILE] is not a notecard's file: a notecard is linked to a .txt, .notecard or .json file. [NAME] carries items, which a file on disk cannot hold, so it cannot be linked to one. - [NAME] is linked to [FILE]: from now on it is changed through the file, and saving the file sends it. + [NAME] is linked to [FILE]: each save of the file sends it. VS Code holds [NAME] too: whichever saves it last is what it holds. - [FILE] is not what [NAME] holds now: the next save of the file sends it, and keeps what [NAME] had in History. + They differ now: the next save sends the file, keeping what [NAME] held in History. [NAME] is no longer linked to [FILE]. Save [NAME] before sending it: what goes up is the file on disk. Sending [NAME] to its script. @@ -2280,8 +2280,8 @@ A notecard dropped on the vimrc box in Script Studio Preferences (Editor tab) is name="notice_text" text_color="AlertCautionTextColor" top="6" - use_ellipses="true" - width="300" /> + width="300" + word_wrap="true" />