summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNat Goodspeed <nat@lindenlab.com>2024-07-09 11:26:47 -0400
committerNat Goodspeed <nat@lindenlab.com>2024-07-09 11:26:47 -0400
commit2a7c68cc0c339ffe5e87fcc506d6312353307e6c (patch)
tree6e7af7cb23b575558f909ee4fb2c42a607fabece
parentb27606feca00172cdfd7467ff3e216824f1c9518 (diff)
parent0b337d5ffec731120adf85da45ceb1ddf5e74621 (diff)
Merge branch 'release/luau-scripting' into lua-snapshot
-rw-r--r--indra/llui/llluafloater.cpp31
-rw-r--r--indra/newview/CMakeLists.txt2
-rw-r--r--indra/newview/llappearancelistener.cpp167
-rw-r--r--indra/newview/llappearancelistener.h46
-rw-r--r--indra/newview/llappearancemgr.cpp62
-rw-r--r--indra/newview/llappearancemgr.h4
-rw-r--r--indra/newview/llviewermenu.cpp4
-rw-r--r--indra/newview/llwearableitemslist.cpp13
-rw-r--r--indra/newview/llwearableitemslist.h8
-rw-r--r--indra/newview/scripts/lua/luafloater_outfits_list.xml51
-rw-r--r--indra/newview/scripts/lua/require/LLAppearance.lua31
-rw-r--r--indra/newview/scripts/lua/test_LLAppearance.lua114
-rw-r--r--indra/newview/skins/default/xui/en/strings.xml4
13 files changed, 517 insertions, 20 deletions
diff --git a/indra/llui/llluafloater.cpp b/indra/llui/llluafloater.cpp
index e584a67a00..b08508bf9b 100644
--- a/indra/llui/llluafloater.cpp
+++ b/indra/llui/llluafloater.cpp
@@ -101,6 +101,15 @@ LLLuaFloater::LLLuaFloater(const LLSD &key) :
}
}, requiredParams);
+ mDispatchListener.add("clear_list", "", [this](const LLSD &event)
+ {
+ LLScrollListCtrl *ctrl = getChild<LLScrollListCtrl>(event["ctrl_name"].asString());
+ if(ctrl)
+ {
+ ctrl->deleteAllItems();
+ }
+ }, llsd::map("ctrl_name", LLSD()));
+
mDispatchListener.add("add_text", "", [this](const LLSD &event)
{
LLTextEditor *editor = getChild<LLTextEditor>(event["ctrl_name"].asString());
@@ -111,15 +120,35 @@ LLLuaFloater::LLLuaFloater(const LLSD &key) :
}
}, requiredParams);
+ mDispatchListener.add("set_label", "", [this](const LLSD &event)
+ {
+ LLButton *btn = getChild<LLButton>(event["ctrl_name"].asString());
+ if (btn)
+ {
+ btn->setLabel((event["value"]).asString());
+ }
+ }, requiredParams);
+
mDispatchListener.add("set_title", "", [this](const LLSD &event)
{
setTitle(event["value"].asString());
}, llsd::map("value", LLSD()));
-
+
mDispatchListener.add("get_value", "", [ctrl_lookup](const LLSD &event)
{
return ctrl_lookup(event, [](LLUICtrl *ctrl, const LLSD &event) { return llsd::map("value", ctrl->getValue()); });
}, llsd::map("ctrl_name", LLSD(), "reqid", LLSD()));
+
+ mDispatchListener.add("get_selected_id", "", [this](const LLSD &event)
+ {
+ LLScrollListCtrl *ctrl = getChild<LLScrollListCtrl>(event["ctrl_name"].asString());
+ if (!ctrl)
+ {
+ LL_WARNS("LuaFloater") << "Control not found: " << event["ctrl_name"] << LL_ENDL;
+ return LLSD();
+ }
+ return llsd::map("value", ctrl->getCurrentID());
+ }, llsd::map("ctrl_name", LLSD(), "reqid", LLSD()));
}
LLLuaFloater::~LLLuaFloater()
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt
index ab65d71916..4e7e072289 100644
--- a/indra/newview/CMakeLists.txt
+++ b/indra/newview/CMakeLists.txt
@@ -95,6 +95,7 @@ set(viewer_SOURCE_FILES
llagentwearables.cpp
llanimstatelabels.cpp
llappcorehttp.cpp
+ llappearancelistener.cpp
llappearancemgr.cpp
llappviewer.cpp
llappviewerlistener.cpp
@@ -760,6 +761,7 @@ set(viewer_HEADER_FILES
llanimstatelabels.h
llappcorehttp.h
llappearance.h
+ llappearancelistener.h
llappearancemgr.h
llappviewer.h
llappviewerlistener.h
diff --git a/indra/newview/llappearancelistener.cpp b/indra/newview/llappearancelistener.cpp
new file mode 100644
index 0000000000..75eaf29186
--- /dev/null
+++ b/indra/newview/llappearancelistener.cpp
@@ -0,0 +1,167 @@
+/**
+ * @file llappearancelistener.cpp
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2024, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+#include "llviewerprecompiledheaders.h"
+
+#include "llappearancelistener.h"
+
+#include "llappearancemgr.h"
+#include "llinventoryfunctions.h"
+#include "lltransutil.h"
+#include "llwearableitemslist.h"
+#include "stringize.h"
+
+LLAppearanceListener::LLAppearanceListener()
+ : LLEventAPI("LLAppearance",
+ "API to wear a specified outfit and wear/remove individual items")
+{
+ add("wearOutfit",
+ "Wear outfit by folder id: [folder_id] OR by folder name: [folder_name]\n"
+ "When [\"append\"] is true, outfit will be added to COF\n"
+ "otherwise it will replace current oufit",
+ &LLAppearanceListener::wearOutfit);
+
+ add("wearItems",
+ "Wear items by id: [items_id]",
+ &LLAppearanceListener::wearItems,
+ llsd::map("items_id", LLSD(), "replace", LLSD()));
+
+ add("detachItems",
+ "Detach items by id: [items_id]",
+ &LLAppearanceListener::detachItems,
+ llsd::map("items_id", LLSD()));
+
+ add("getOutfitsList",
+ "Return the table with Outfits info(id and name)",
+ &LLAppearanceListener::getOutfitsList);
+
+ add("getOutfitItems",
+ "Return the table of items with info(id : name, wearable_type, is_worn) inside specified outfit folder",
+ &LLAppearanceListener::getOutfitItems);
+}
+
+
+void LLAppearanceListener::wearOutfit(LLSD const &data)
+{
+ Response response(LLSD(), data);
+ if (!data.has("folder_id") && !data.has("folder_name"))
+ {
+ return response.error("Either [folder_id] or [folder_name] is required");
+ }
+
+ std::string error_msg;
+ bool result(false);
+ bool append = data.has("append") ? data["append"].asBoolean() : false;
+ if (data.has("folder_id"))
+ {
+ result = LLAppearanceMgr::instance().wearOutfit(data["folder_id"].asUUID(), error_msg, append);
+ }
+ else
+ {
+ result = LLAppearanceMgr::instance().wearOutfitByName(data["folder_name"].asString(), error_msg, append);
+ }
+
+ if (!result)
+ {
+ response.error(error_msg);
+ }
+}
+
+void LLAppearanceListener::wearItems(LLSD const &data)
+{
+ if (data["items_id"].isArray())
+ {
+ uuid_vec_t ids;
+ for (const auto &id : llsd::inArray(data["items_id"]))
+ {
+ ids.push_back(id);
+ }
+ LLAppearanceMgr::instance().wearItemsOnAvatar(ids, true, data["replace"].asBoolean());
+ }
+ else
+ {
+ LLAppearanceMgr::instance().wearItemOnAvatar(data["items_id"].asUUID(), true, data["replace"].asBoolean());
+ }
+}
+
+void LLAppearanceListener::detachItems(LLSD const &data)
+{
+ if (data["items_id"].isArray())
+ {
+ uuid_vec_t ids;
+ for (const auto &id : llsd::inArray(data["items_id"]))
+ {
+ ids.push_back(id);
+ }
+ LLAppearanceMgr::instance().removeItemsFromAvatar(ids);
+ }
+ else
+ {
+ LLAppearanceMgr::instance().removeItemFromAvatar(data["items_id"].asUUID());
+ }
+}
+
+void LLAppearanceListener::getOutfitsList(LLSD const &data)
+{
+ Response response(LLSD(), data);
+ const LLUUID outfits_id = gInventory.findCategoryUUIDForType(LLFolderType::FT_MY_OUTFITS);
+
+ LLInventoryModel::cat_array_t cat_array;
+ LLInventoryModel::item_array_t item_array;
+
+ LLIsType is_category(LLAssetType::AT_CATEGORY);
+ gInventory.collectDescendentsIf(outfits_id, cat_array, item_array, LLInventoryModel::EXCLUDE_TRASH, is_category);
+
+ response["outfits"] = llsd::toMap(cat_array,
+ [](const LLPointer<LLViewerInventoryCategory> &cat)
+ { return std::make_pair(cat->getUUID().asString(), cat->getName()); });
+}
+
+void LLAppearanceListener::getOutfitItems(LLSD const &data)
+{
+ Response response(LLSD(), data);
+ LLUUID outfit_id(data["outfit_id"].asUUID());
+ LLViewerInventoryCategory *cat = gInventory.getCategory(outfit_id);
+ if (!cat || cat->getPreferredType() != LLFolderType::FT_OUTFIT)
+ {
+ return response.error(stringize(LLTrans::getString("OutfitNotFound"), outfit_id.asString()));
+ }
+ LLInventoryModel::cat_array_t cat_array;
+ LLInventoryModel::item_array_t item_array;
+
+ LLFindOutfitItems collector = LLFindOutfitItems();
+ gInventory.collectDescendentsIf(outfit_id, cat_array, item_array, LLInventoryModel::EXCLUDE_TRASH, collector);
+
+ response["items"] = llsd::toMap(item_array,
+ [](const LLPointer<LLViewerInventoryItem> &it)
+ {
+ return std::make_pair(
+ it->getUUID().asString(),
+ llsd::map(
+ "name", it->getName(),
+ "wearable_type", LLWearableType::getInstance()->getTypeName(it->isWearableType() ? it->getWearableType() : LLWearableType::WT_NONE),
+ "is_worn", get_is_item_worn(it)));
+ });
+}
diff --git a/indra/newview/llappearancelistener.h b/indra/newview/llappearancelistener.h
new file mode 100644
index 0000000000..04c5eac2eb
--- /dev/null
+++ b/indra/newview/llappearancelistener.h
@@ -0,0 +1,46 @@
+/**
+ * @file llappearancelistener.h
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2024, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+
+#ifndef LL_LLAPPEARANCELISTENER_H
+#define LL_LLAPPEARANCELISTENER_H
+
+#include "lleventapi.h"
+
+class LLAppearanceListener : public LLEventAPI
+{
+public:
+ LLAppearanceListener();
+
+private:
+ void wearOutfit(LLSD const &data);
+ void wearItems(LLSD const &data);
+ void detachItems(LLSD const &data);
+ void getOutfitsList(LLSD const &data);
+ void getOutfitItems(LLSD const &data);
+};
+
+#endif // LL_LLAPPEARANCELISTENER_H
+
diff --git a/indra/newview/llappearancemgr.cpp b/indra/newview/llappearancemgr.cpp
index 30f07a873b..7a34006323 100644
--- a/indra/newview/llappearancemgr.cpp
+++ b/indra/newview/llappearancemgr.cpp
@@ -31,6 +31,7 @@
#include "llagent.h"
#include "llagentcamera.h"
#include "llagentwearables.h"
+#include "llappearancelistener.h"
#include "llappearancemgr.h"
#include "llattachmentsmgr.h"
#include "llcommandhandler.h"
@@ -48,6 +49,7 @@
#include "lloutfitslist.h"
#include "llselectmgr.h"
#include "llsidepanelappearance.h"
+#include "lltransutil.h"
#include "llviewerobjectlist.h"
#include "llvoavatar.h"
#include "llvoavatarself.h"
@@ -71,6 +73,8 @@
#pragma warning (disable:4702)
#endif
+LLAppearanceListener sAppearanceListener;
+
namespace
{
const S32 BAKE_RETRY_MAX_COUNT = 5;
@@ -2900,8 +2904,7 @@ void LLAppearanceMgr::wearInventoryCategoryOnAvatar( LLInventoryCategory* catego
LLAppearanceMgr::changeOutfit(TRUE, category->getUUID(), append);
}
-// FIXME do we really want to search entire inventory for matching name?
-void LLAppearanceMgr::wearOutfitByName(const std::string& name)
+bool LLAppearanceMgr::wearOutfitByName(const std::string& name, std::string& error_msg, bool append)
{
LL_INFOS("Avatar") << self_av_string() << "Wearing category " << name << LL_ENDL;
@@ -2936,13 +2939,62 @@ void LLAppearanceMgr::wearOutfitByName(const std::string& name)
if(cat)
{
- LLAppearanceMgr::wearInventoryCategory(cat, copy_items, false);
+ // don't allow wearing a system folder
+ if (LLFolderType::lookupIsProtectedType(cat->getPreferredType()))
+ {
+ error_msg = stringize(LLTrans::getString("SystemFolderNotWorn"), std::quoted(name));
+ return false;
+ }
+ bool can_wear = append ? getCanAddToCOF(cat->getUUID()) : getCanReplaceCOF(cat->getUUID());
+ if (!can_wear)
+ {
+ std::string msg = append ? LLTrans::getString("OutfitNotAdded") : LLTrans::getString("OutfitNotReplaced");
+ error_msg = stringize(msg, std::quoted(name), ", id: ", cat->getUUID());
+ return false;
+ }
+ LLAppearanceMgr::wearInventoryCategory(cat, copy_items, append);
}
else
{
- LL_WARNS() << "Couldn't find outfit " <<name<< " in wearOutfitByName()"
- << LL_ENDL;
+ error_msg = stringize(LLTrans::getString("OutfitNotFound"), std::quoted(name));
+ return false;
+ }
+ return true;
+}
+
+bool LLAppearanceMgr::wearOutfit(const LLUUID &cat_id, std::string &error_msg, bool append)
+{
+ LLViewerInventoryCategory *cat = gInventory.getCategory(cat_id);
+ if (!cat)
+ {
+ error_msg = stringize(LLTrans::getString("OutfitNotFound"), cat_id);
+ return false;
+ }
+ if (LLFolderType::lookupIsProtectedType(cat->getPreferredType()))
+ {
+ error_msg = stringize(LLTrans::getString("SystemFolderNotWorn"), cat_id);
+ return false;
}
+ bool can_wear = append ? LLAppearanceMgr::instance().getCanAddToCOF(cat_id) : LLAppearanceMgr::instance().getCanReplaceCOF(cat_id);
+ if (!can_wear)
+ {
+ std::string msg = append ? LLTrans::getString("OutfitNotAdded") : LLTrans::getString("OutfitNotReplaced");
+ error_msg = stringize(msg, std::quoted(cat->getName()), " , id: ", cat_id);
+ return false;
+ }
+ LLAppearanceMgr::instance().wearInventoryCategory(cat, false, append);
+ return true;
+}
+
+bool LLAppearanceMgr::wearOutfitByName(const std::string& name, bool append)
+{
+ std::string error_msg;
+ if(!wearOutfitByName(name, error_msg, append))
+ {
+ LL_WARNS() << error_msg << LL_ENDL;
+ return false;
+ }
+ return true;
}
bool areMatchingWearables(const LLViewerInventoryItem *a, const LLViewerInventoryItem *b)
diff --git a/indra/newview/llappearancemgr.h b/indra/newview/llappearancemgr.h
index e5de92b653..b795494f94 100644
--- a/indra/newview/llappearancemgr.h
+++ b/indra/newview/llappearancemgr.h
@@ -59,7 +59,9 @@ public:
void wearInventoryCategory(LLInventoryCategory* category, bool copy, bool append);
void wearInventoryCategoryOnAvatar(LLInventoryCategory* category, bool append);
void wearCategoryFinal(const LLUUID& cat_id, bool copy_items, bool append);
- void wearOutfitByName(const std::string& name);
+ bool wearOutfit(const LLUUID &cat_id, std::string &error_msg, bool append = false);
+ bool wearOutfitByName(const std::string &name, std::string &error_msg, bool append = false);
+ bool wearOutfitByName(const std::string &name, bool append = false);
void changeOutfit(bool proceed, const LLUUID& category, bool append);
void replaceCurrentOutfit(const LLUUID& new_outfit);
void renameOutfit(const LLUUID& outfit_id);
diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp
index 90b7c43047..cea7180187 100644
--- a/indra/newview/llviewermenu.cpp
+++ b/indra/newview/llviewermenu.cpp
@@ -9745,8 +9745,8 @@ void initialize_menus()
view_listener_t::addMenu(new LLAdvancedEnableAppearanceToXML(), "Advanced.EnableAppearanceToXML");
view_listener_t::addMenu(new LLAdvancedToggleCharacterGeometry(), "Advanced.ToggleCharacterGeometry");
- view_listener_t::addMenu(new LLAdvancedTestMale(), "Advanced.TestMale");
- view_listener_t::addMenu(new LLAdvancedTestFemale(), "Advanced.TestFemale");
+ view_listener_t::addMenu(new LLAdvancedTestMale(), "Advanced.TestMale", cb_info::UNTRUSTED_THROTTLE);
+ view_listener_t::addMenu(new LLAdvancedTestFemale(), "Advanced.TestFemale", cb_info::UNTRUSTED_THROTTLE);
// Advanced > Character > Animation Speed
view_listener_t::addMenu(new LLAdvancedAnimTenFaster(), "Advanced.AnimTenFaster");
diff --git a/indra/newview/llwearableitemslist.cpp b/indra/newview/llwearableitemslist.cpp
index 676164fcc6..e122cc0360 100644
--- a/indra/newview/llwearableitemslist.cpp
+++ b/indra/newview/llwearableitemslist.cpp
@@ -33,7 +33,6 @@
#include "llagentwearables.h"
#include "llappearancemgr.h"
-#include "llinventoryfunctions.h"
#include "llinventoryicon.h"
#include "llgesturemgr.h"
#include "lltransutil.h"
@@ -41,14 +40,6 @@
#include "llviewermenu.h"
#include "llvoavatarself.h"
-class LLFindOutfitItems : public LLInventoryCollectFunctor
-{
-public:
- LLFindOutfitItems() {}
- virtual ~LLFindOutfitItems() {}
- virtual bool operator()(LLInventoryCategory* cat,
- LLInventoryItem* item);
-};
bool LLFindOutfitItems::operator()(LLInventoryCategory* cat,
LLInventoryItem* item)
@@ -60,10 +51,10 @@ bool LLFindOutfitItems::operator()(LLInventoryCategory* cat,
|| (item->getType() == LLAssetType::AT_OBJECT)
|| (item->getType() == LLAssetType::AT_GESTURE))
{
- return TRUE;
+ return true;
}
}
- return FALSE;
+ return false;
}
//////////////////////////////////////////////////////////////////////////
diff --git a/indra/newview/llwearableitemslist.h b/indra/newview/llwearableitemslist.h
index 80e211ad6b..15033f5e9a 100644
--- a/indra/newview/llwearableitemslist.h
+++ b/indra/newview/llwearableitemslist.h
@@ -32,6 +32,7 @@
#include "llsingleton.h"
// newview
+#include "llinventoryfunctions.h"
#include "llinventoryitemslist.h"
#include "llinventorylistitem.h"
#include "lllistcontextmenu.h"
@@ -505,4 +506,11 @@ protected:
LLWearableType::EType mMenuWearableType;
};
+struct LLFindOutfitItems : public LLInventoryCollectFunctor
+{
+ LLFindOutfitItems() {}
+ virtual ~LLFindOutfitItems() {}
+ virtual bool operator()(LLInventoryCategory *cat, LLInventoryItem *item);
+};
+
#endif //LL_LLWEARABLEITEMSLIST_H
diff --git a/indra/newview/scripts/lua/luafloater_outfits_list.xml b/indra/newview/scripts/lua/luafloater_outfits_list.xml
new file mode 100644
index 0000000000..8cab864308
--- /dev/null
+++ b/indra/newview/scripts/lua/luafloater_outfits_list.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<floater
+ legacy_header_height="18"
+ height="205"
+ layout="topleft"
+ name="lua_outfits"
+ title="Outfits"
+ width="325">
+ <scroll_list
+ draw_heading="false"
+ left="5"
+ width="315"
+ height="150"
+ top_pad ="25"
+ follows="all"
+ name="outfits_list">
+ <scroll_list.columns
+ name="outfit_name"
+ label="Name"/>
+ </scroll_list>
+ <button
+ follows="left|bottom"
+ height="23"
+ label="Replace COF"
+ layout="topleft"
+ name="replace_btn"
+ enabled="false"
+ top_pad="5"
+ width="95" >
+ </button>
+ <button
+ follows="left|bottom"
+ height="23"
+ label="Add to COF"
+ left_pad="7"
+ layout="topleft"
+ name="add_btn"
+ enabled="false"
+ width="95" >
+ </button>
+ <button
+ follows="left|bottom"
+ height="23"
+ label="Show wearables"
+ left_pad="7"
+ layout="topleft"
+ name="select_btn"
+ enabled="false"
+ width="111" >
+ </button>
+</floater>
diff --git a/indra/newview/scripts/lua/require/LLAppearance.lua b/indra/newview/scripts/lua/require/LLAppearance.lua
new file mode 100644
index 0000000000..f533d22daf
--- /dev/null
+++ b/indra/newview/scripts/lua/require/LLAppearance.lua
@@ -0,0 +1,31 @@
+local leap = require 'leap'
+
+local LLAppearance = {}
+
+function LLAppearance.wearOutfit(folder, action)
+ action = action or 'add'
+ leap.request('LLAppearance', {op='wearOutfit', append = (action == 'add'), folder_id=folder})
+end
+
+function LLAppearance.wearOutfitByName(folder, action)
+ action = action or 'add'
+ leap.request('LLAppearance', {op='wearOutfit', append = (action == 'add'), folder_name=folder})
+end
+
+function LLAppearance.wearItems(items_id, replace)
+ leap.send('LLAppearance', {op='wearItems', replace = replace, items_id=items_id})
+end
+
+function LLAppearance.detachItems(items_id)
+ leap.send('LLAppearance', {op='detachItems', items_id=items_id})
+end
+
+function LLAppearance.getOutfitsList()
+ return leap.request('LLAppearance', {op='getOutfitsList'})['outfits']
+end
+
+function LLAppearance.getOutfitItems(id)
+ return leap.request('LLAppearance', {op='getOutfitItems', outfit_id = id})['items']
+end
+
+return LLAppearance
diff --git a/indra/newview/scripts/lua/test_LLAppearance.lua b/indra/newview/scripts/lua/test_LLAppearance.lua
new file mode 100644
index 0000000000..5ddd9f15ff
--- /dev/null
+++ b/indra/newview/scripts/lua/test_LLAppearance.lua
@@ -0,0 +1,114 @@
+local Floater = require 'Floater'
+local LLAppearance = require 'LLAppearance'
+local startup = require 'startup'
+local inspect = require 'inspect'
+
+local SHOW_OUTFITS = true
+local SELECTED_OUTFIT_ID = {}
+local DATA_MAP = {}
+
+local wearables_lbl = 'Show wearables'
+local outfits_lbl = 'Show outfits'
+local replace_cof_lbl = 'Replace COF'
+local add_cof_lbl = 'Add to COF'
+local outfits_title = 'Outfits'
+local wear_lbl = 'Wear item'
+local detach_lbl = 'Detach item'
+
+local flt = Floater:new(
+ "luafloater_outfits_list.xml",
+ {outfits_list = {"double_click"}})
+
+function get_selected_id()
+ return flt:request({action="get_selected_id", ctrl_name='outfits_list'}).value
+end
+
+function populate_list()
+ if SHOW_OUTFITS then
+ DATA_MAP = LLAppearance.getOutfitsList()
+ else
+ DATA_MAP = LLAppearance.getOutfitItems(SELECTED_OUTFIT_ID)
+ end
+
+ local action_data = {}
+ action_data.action = "add_list_element"
+ action_data.ctrl_name = "outfits_list"
+ local outfits = {}
+ for uuid, info in pairs(DATA_MAP) do
+ name = {}
+ if SHOW_OUTFITS then
+ name = info
+ else
+ name = info.name
+ end
+ table.insert(outfits, {value = uuid, columns={column = "outfit_name", value = name}})
+ end
+ action_data.value = outfits
+ flt:post(action_data)
+end
+
+function set_label(btn_name, value)
+ flt:post({action="set_label", ctrl_name=btn_name, value=value})
+end
+
+function set_enabled(btn_name, value)
+ flt:post({action="set_enabled", ctrl_name=btn_name, value=value})
+end
+
+function update_labels()
+ if SHOW_OUTFITS then
+ set_label('select_btn', wearables_lbl)
+ set_label('replace_btn', replace_cof_lbl)
+ set_label('add_btn', add_cof_lbl)
+
+ set_enabled('select_btn', false)
+ flt:post({action="set_title", value=outfits_title})
+ else
+ set_label('select_btn', outfits_lbl)
+ set_label('replace_btn', wear_lbl)
+ set_label('add_btn', detach_lbl)
+
+ set_enabled('select_btn', true)
+ flt:post({action="set_title", value=DATA_MAP[SELECTED_OUTFIT_ID]})
+ end
+
+ set_enabled('replace_btn', false)
+ set_enabled('add_btn', false)
+end
+
+function flt:post_build(event_data)
+ populate_list()
+end
+
+function flt:commit_replace_btn(event_data)
+ if SHOW_OUTFITS then
+ LLAppearance.wearOutfit(get_selected_id(), 'replace')
+ else
+ LLAppearance.wearItems(get_selected_id(), false)
+ end
+end
+
+function flt:commit_add_btn(event_data)
+ if SHOW_OUTFITS then
+ LLAppearance.wearOutfit(get_selected_id(), 'add')
+ else
+ LLAppearance.detachItems(get_selected_id())
+ end
+end
+
+function flt:commit_select_btn(event_data)
+ SHOW_OUTFITS = not SHOW_OUTFITS
+ SELECTED_OUTFIT_ID = get_selected_id()
+ update_labels()
+ self:post({action="clear_list", ctrl_name='outfits_list'})
+ populate_list()
+end
+
+function flt:commit_outfits_list(event_data)
+ set_enabled('replace_btn', true)
+ set_enabled('add_btn', true)
+ set_enabled('select_btn', true)
+end
+
+startup.wait('STATE_STARTED')
+flt:show()
diff --git a/indra/newview/skins/default/xui/en/strings.xml b/indra/newview/skins/default/xui/en/strings.xml
index 76a2660dbb..492b29fbc7 100644
--- a/indra/newview/skins/default/xui/en/strings.xml
+++ b/indra/newview/skins/default/xui/en/strings.xml
@@ -4028,6 +4028,10 @@ Please check http://status.secondlifegrid.net to see if there is a known problem
<string name="DeleteItem">Delete selected item?</string>
<string name="EmptyOutfitText">There are no items in this outfit</string>
+ <string name="OutfitNotFound" value="Couldn't find outfit "/>
+ <string name="OutfitNotAdded" value="Can't add to COF outfit "/>
+ <string name="OutfitNotReplaced" value="Can't replace COF with outfit "/>
+ <string name="SystemFolderNotWorn" value="Can't wear system folder "/>
<!-- External editor status codes -->
<string name="ExternalEditorNotSet">Select an editor by setting the environment variable LL_SCRIPT_EDITOR or the ExternalEditor setting.