From 10276ee967a5ad860fbf8750ce1012db3c5ae78a Mon Sep 17 00:00:00 2001
From: Callum Prentice <callum@lindenlab.com>
Date: Thu, 17 Aug 2023 15:49:50 -0700
Subject: [PATCH] Small updates to bulky thumbs but mainly, this is about the
 first version of the Inventory Thumbnail Helper tool - see source file for a
 link to Confluence page listing how to use it

---
 indra/newview/llfloaterbulkythumbs.cpp        |  55 ++-
 indra/newview/llfloaterbulkythumbs.h          |   3 +
 .../llfloaterinventorythumbnailshelper.cpp    | 416 +++++++++++-------
 .../llfloaterinventorythumbnailshelper.h      |  28 +-
 .../default/xui/en/floater_bulky_thumbs.xml   |  15 +-
 .../floater_inventory_thumbnails_helper.xml   |  33 +-
 .../skins/default/xui/en/notifications.xml    |  14 +
 7 files changed, 383 insertions(+), 181 deletions(-)

diff --git a/indra/newview/llfloaterbulkythumbs.cpp b/indra/newview/llfloaterbulkythumbs.cpp
index 9ffb71b394c..02adf90acf7 100644
--- a/indra/newview/llfloaterbulkythumbs.cpp
+++ b/indra/newview/llfloaterbulkythumbs.cpp
@@ -70,6 +70,10 @@ BOOL LLFloaterBulkyThumbs::postBuild()
     mWriteThumbnailsBtn->setCommitCallback(boost::bind(&LLFloaterBulkyThumbs::onWriteThumbnails, this));
     mWriteThumbnailsBtn->setEnabled(false);
 
+    mDisplayThumbnaillessItemsBtn = getChild<LLUICtrl>("display_thumbless_items");
+    mDisplayThumbnaillessItemsBtn->setCommitCallback(boost::bind(&LLFloaterBulkyThumbs::onDisplayThumbnaillessItems, this));
+    mDisplayThumbnaillessItemsBtn->setEnabled(true);
+
     return true;
 }
 
@@ -96,6 +100,18 @@ void LLFloaterBulkyThumbs::recordInventoryItemEntry(LLViewerInventoryItem* item)
     {
         // dupe - do not save
     }
+
+    if (item->getThumbnailUUID() == LLUUID::null)
+    {
+        mOutputLog->appendText(
+            STRINGIZE(
+                "ITEM " << mItemNamesIDs.size() << "> " <<
+                name <<
+                " has no thumbnail" <<
+                //id.asString() <<
+                std::endl
+            ), false);
+    }
 }
 
 void LLFloaterBulkyThumbs::onPasteItems()
@@ -139,6 +155,13 @@ void LLFloaterBulkyThumbs::onPasteItems()
                                             LLInventoryModel::EXCLUDE_TRASH,
                                             is_bodypart);
 
+            LLIsType is_clothing(LLAssetType::AT_CLOTHING);
+            gInventory.collectDescendentsIf(cat->getUUID(),
+                                            cat_array,
+                                            item_array,
+                                            LLInventoryModel::EXCLUDE_TRASH,
+                                            is_clothing);
+
             for (size_t i = 0; i < item_array.size(); i++)
             {
                 LLViewerInventoryItem* item = item_array.at(i);
@@ -150,7 +173,7 @@ void LLFloaterBulkyThumbs::onPasteItems()
         if (item)
         {
             const LLAssetType::EType item_type = item->getType();
-            if (item_type == LLAssetType::AT_OBJECT || item_type == LLAssetType::AT_BODYPART)
+            if (item_type == LLAssetType::AT_OBJECT || item_type == LLAssetType::AT_BODYPART || item_type == LLAssetType::AT_CLOTHING)
             {
                 recordInventoryItemEntry(item);
             }
@@ -388,3 +411,33 @@ void LLFloaterBulkyThumbs::onWriteThumbnails()
     }
     mOutputLog->setCursorAndScrollToEnd();
 }
+
+void LLFloaterBulkyThumbs::onDisplayThumbnaillessItems()
+{
+    //std::map<std::string, LLUUID>::iterator item_iter = mItemNamesIDs.begin();
+    //size_t index = 1;
+
+    //mOutputLog->appendText("======= Items with no thumbnail =======", false);
+
+    //while (item_iter != mItemNamesIDs.end())
+    //{
+    //    std::string item_name = (*item_iter).first;
+
+    //    //mOutputLog->appendText(
+    //    //    STRINGIZE(
+    //    //        "MATCHING ITEM (" << index++  << "/" << mItemNamesIDs.size() << ") " << item_name << "> "
+    //    //    ), false);
+
+    //    std::map<std::string, LLUUID>::iterator texture_iter = mTextureNamesIDs.find(item_name);
+    //    if (texture_iter == mTextureNamesIDs.end())
+    //    {
+    //        mOutputLog->appendText(
+    //            STRINGIZE(
+    //                "MISSING:" <<
+
+    //                std::endl
+    //            ), false);
+
+    //    }
+    //}
+}
diff --git a/indra/newview/llfloaterbulkythumbs.h b/indra/newview/llfloaterbulkythumbs.h
index 75657002b36..10133553a6b 100644
--- a/indra/newview/llfloaterbulkythumbs.h
+++ b/indra/newview/llfloaterbulkythumbs.h
@@ -57,6 +57,9 @@ class LLFloaterBulkyThumbs:
         LLUICtrl* mWriteThumbnailsBtn;
         void onWriteThumbnails();
 
+        LLUICtrl* mDisplayThumbnaillessItemsBtn;
+        void onDisplayThumbnaillessItems();
+
         void recordInventoryItemEntry(LLViewerInventoryItem* item);
         void recordTextureItemEntry(LLViewerInventoryItem* item);
 
diff --git a/indra/newview/llfloaterinventorythumbnailshelper.cpp b/indra/newview/llfloaterinventorythumbnailshelper.cpp
index 9697fc3d519..15e1c88572c 100644
--- a/indra/newview/llfloaterinventorythumbnailshelper.cpp
+++ b/indra/newview/llfloaterinventorythumbnailshelper.cpp
@@ -3,6 +3,9 @@
  * @author Callum Prentice
  * @brief LLFloaterInventoryThumbnailsHelper class implementation
  *
+ * Usage instructions and some brief notes can be found in Confluence here:
+ * https://lindenlab.atlassian.net/wiki/spaces/~174746736/pages/2928672843/Inventory+Thumbnail+Helper+Tool
+ * 
  * $LicenseInfo:firstyear=2008&license=viewerlgpl$
  * Second Life Viewer Source Code
  * Copyright (C) 2010, Linden Research, Inc.
@@ -25,23 +28,20 @@
  * $/LicenseInfo$
  */
 
-/**
- * Floater that appears when buying an object, giving a preview
- * of its contents and their permissions.
- */
-
 #include "llviewerprecompiledheaders.h"
 
-#include "llfloaterinventorythumbnailshelper.h"
-#include "lluictrlfactory.h"
+#include "llaisapi.h"
 #include "llclipboard.h"
-#include "llinventorymodel.h"
 #include "llinventoryfunctions.h"
-#include "lltexteditor.h"
+#include "llinventorymodel.h"
+#include "llnotifications.h"
+#include "llnotificationsutil.h"
 #include "llscrolllistctrl.h"
-#include "llmediactrl.h"
+#include "lltexteditor.h"
+#include "lluictrlfactory.h"
 #include "lluuid.h"
-#include "llaisapi.h"
+
+#include "llfloaterinventorythumbnailshelper.h"
 
 LLFloaterInventoryThumbnailsHelper::LLFloaterInventoryThumbnailsHelper(const LLSD& key)
     :   LLFloater("floater_inventory_thumbnails_helper")
@@ -54,66 +54,53 @@ LLFloaterInventoryThumbnailsHelper::~LLFloaterInventoryThumbnailsHelper()
 
 BOOL LLFloaterInventoryThumbnailsHelper::postBuild()
 {
-    mPasteItemsBtn = getChild<LLUICtrl>("paste_items_btn");
-    mPasteItemsBtn->setCommitCallback(boost::bind(&LLFloaterInventoryThumbnailsHelper::onPasteItems, this));
-
-    mPasteTexturesBtn = getChild<LLUICtrl>("paste_textures_btn");
-    mPasteTexturesBtn->setCommitCallback(boost::bind(&LLFloaterInventoryThumbnailsHelper::onPasteTextures, this));
+    mInventoryThumbnailsList = getChild<LLScrollListCtrl>("inventory_thumbnails_list");
+    mInventoryThumbnailsList->setAllowMultipleSelection(true);
 
     mOutputLog = getChild<LLTextEditor>("output_log");
     mOutputLog->setMaxTextLength(0xffff * 0x10);
 
-    //mMergeItemsTexturesBtn = getChild<LLUICtrl>("merge_items_textures");
-    //mMergeItemsTexturesBtn->setCommitCallback(boost::bind(&LLFloaterInventoryThumbnailsHelper::onMergeItemsTextures, this));
-    //mMergeItemsTexturesBtn->setEnabled(false);
+    mPasteItemsBtn = getChild<LLUICtrl>("paste_items_btn");
+    mPasteItemsBtn->setCommitCallback(boost::bind(&LLFloaterInventoryThumbnailsHelper::onPasteItems, this));
+    mPasteItemsBtn->setEnabled(true);
 
-    mInventoryThumbnailsList = getChild<LLScrollListCtrl>("inventory_thumbnails_list");
-    mInventoryThumbnailsList->setAllowMultipleSelection(true);
-    mInventoryThumbnailsList->deleteAllItems();
+    mPasteTexturesBtn = getChild<LLUICtrl>("paste_textures_btn");
+    mPasteTexturesBtn->setCommitCallback(boost::bind(&LLFloaterInventoryThumbnailsHelper::onPasteTextures, this));
+    mPasteTexturesBtn->setEnabled(true);
 
     mWriteThumbnailsBtn = getChild<LLUICtrl>("write_thumbnails_btn");
     mWriteThumbnailsBtn->setCommitCallback(boost::bind(&LLFloaterInventoryThumbnailsHelper::onWriteThumbnails, this));
     mWriteThumbnailsBtn->setEnabled(false);
 
+    mLogMissingThumbnailsBtn = getChild<LLUICtrl>("log_missing_thumbnails_btn");
+    mLogMissingThumbnailsBtn->setCommitCallback(boost::bind(&LLFloaterInventoryThumbnailsHelper::onLogMissingThumbnails, this));
+    mLogMissingThumbnailsBtn->setEnabled(false);
+
+    mClearThumbnailsBtn = getChild<LLUICtrl>("clear_thumbnails_btn");
+    mClearThumbnailsBtn->setCommitCallback(boost::bind(&LLFloaterInventoryThumbnailsHelper::onClearThumbnails, this));
+    mClearThumbnailsBtn->setEnabled(false);
+
     return true;
 }
 
+// Records an entry in the pasted items - saves it to a map and writes it to the log
+// window for later confirmation/validation - since it uses a map, duplicates (based on
+// the name) are discarded
 void LLFloaterInventoryThumbnailsHelper::recordInventoryItemEntry(LLViewerInventoryItem* item)
 {
     const std::string name = item->getName();
 
-    std::map<std::string, LLUUID>::iterator iter = mItemNamesIDs.find(name);
-    if (iter == mItemNamesIDs.end())
+    std::map<std::string, LLViewerInventoryItem*>::iterator iter = mItemNamesItems.find(name);
+    if (iter == mItemNamesItems.end())
     {
-        LLUUID id = item->getUUID();
-        mItemNamesIDs.insert({name, id});
+        mItemNamesItems.insert({name, item});
 
-        mOutputLog->appendText(
+        writeToLog(
             STRINGIZE(
-                "ITEM " << mItemNamesIDs.size() << "> " <<
+                "ITEM " << mItemNamesItems.size() << "> " <<
                 name <<
-                //" | " <<
-                //id.asString() <<
                 std::endl
             ), false);
-
-        // TODO: use this ID to get name of texture and display that
-        const LLUUID current_thumbnail_id = item->getThumbnailUUID();
-
-        std::string texture_display = std::string("Not Present");
-        if (!current_thumbnail_id.isNull())
-        {
-            texture_display = current_thumbnail_id.asString();
-        }
-
-        LLSD row;
-        row["columns"][0]["column"] = "name";
-        row["columns"][0]["type"] = "text";
-        row["columns"][0]["value"] = name;
-        row["columns"][1]["column"] = "texture";
-        row["columns"][1]["type"] = "text";
-        row["columns"][1]["value"] = texture_display;
-        mInventoryThumbnailsList->addElement(row);
     }
     else
     {
@@ -121,6 +108,10 @@ void LLFloaterInventoryThumbnailsHelper::recordInventoryItemEntry(LLViewerInvent
     }
 }
 
+// Called when the user has copied items from their inventory and selects the Paste Items button
+// in the UI - iterates over items and folders and saves details of each one.
+// The first use of this tool is for updating NUX items and as such, only looks for OBJECTS,
+// CLOTHING and BODYPARTS - later versions of this tool should make that selection editable.
 void LLFloaterInventoryThumbnailsHelper::onPasteItems()
 {
     if (!LLClipboard::instance().hasContents())
@@ -128,7 +119,7 @@ void LLFloaterInventoryThumbnailsHelper::onPasteItems()
         return;
     }
 
-    mOutputLog->appendText(
+    writeToLog(
         STRINGIZE(
             "\n==== Pasting items from inventory ====" <<
             std::endl
@@ -142,6 +133,7 @@ void LLFloaterInventoryThumbnailsHelper::onPasteItems()
     {
         const LLUUID& entry = objects.at(i);
 
+        // Check for a folder
         const LLInventoryCategory* cat = gInventory.getCategory(entry);
         if (cat)
         {
@@ -162,6 +154,13 @@ void LLFloaterInventoryThumbnailsHelper::onPasteItems()
                                             LLInventoryModel::EXCLUDE_TRASH,
                                             is_bodypart);
 
+            LLIsType is_clothing(LLAssetType::AT_CLOTHING);
+            gInventory.collectDescendentsIf(cat->getUUID(),
+                                            cat_array,
+                                            item_array,
+                                            LLInventoryModel::EXCLUDE_TRASH,
+                                            is_clothing);
+
             for (size_t i = 0; i < item_array.size(); i++)
             {
                 LLViewerInventoryItem* item = item_array.at(i);
@@ -169,20 +168,28 @@ void LLFloaterInventoryThumbnailsHelper::onPasteItems()
             }
         }
 
+        // Check for an item
         LLViewerInventoryItem* item = gInventory.getItem(entry);
         if (item)
         {
             const LLAssetType::EType item_type = item->getType();
-            if (item_type == LLAssetType::AT_OBJECT || item_type == LLAssetType::AT_BODYPART)
+            if (item_type == LLAssetType::AT_OBJECT || item_type == LLAssetType::AT_BODYPART || item_type == LLAssetType::AT_CLOTHING)
             {
                 recordInventoryItemEntry(item);
             }
         }
     }
 
-    mOutputLog->setCursorAndScrollToEnd();
+    // update the main list view based on what we found
+    updateDisplayList();
+
+    // update the buttons enabled state based on what we found/saved
+    updateButtonStates();
 }
 
+// Records a entry in the pasted textures - saves it to a map and writes it to the log
+// window for later confirmation/validation - since it uses a map, duplicates (based on
+// the name) are discarded
 void LLFloaterInventoryThumbnailsHelper::recordTextureItemEntry(LLViewerInventoryItem* item)
 {
     const std::string name = item->getName();
@@ -193,7 +200,7 @@ void LLFloaterInventoryThumbnailsHelper::recordTextureItemEntry(LLViewerInventor
         LLUUID id = item->getAssetUUID();
         mTextureNamesIDs.insert({name, id});
 
-        mOutputLog->appendText(
+        writeToLog(
             STRINGIZE(
                 "TEXTURE " << mTextureNamesIDs.size() << "> " <<
                 name <<
@@ -208,6 +215,8 @@ void LLFloaterInventoryThumbnailsHelper::recordTextureItemEntry(LLViewerInventor
     }
 }
 
+// Called when the user has copied textures from their inventory and selects the Paste Textures
+// button in the UI - iterates over textures and folders and saves details of each one.
 void LLFloaterInventoryThumbnailsHelper::onPasteTextures()
 {
     if (!LLClipboard::instance().hasContents())
@@ -215,7 +224,7 @@ void LLFloaterInventoryThumbnailsHelper::onPasteTextures()
         return;
     }
 
-    mOutputLog->appendText(
+    writeToLog(
         STRINGIZE(
             "\n==== Pasting textures from inventory ====" <<
             std::endl
@@ -260,105 +269,65 @@ void LLFloaterInventoryThumbnailsHelper::onPasteTextures()
         }
     }
 
-    mOutputLog->setCursorAndScrollToEnd();
+    // update the main list view based on what we found
+    updateDisplayList();
 
-    populateThumbnailNames();
+    // update the buttons enabled state based on what we found/saved
+    updateButtonStates();
 }
 
-
-void LLFloaterInventoryThumbnailsHelper::populateThumbnailNames()
+// Updates the main list of entries in the UI based on what is in the maps/storage
+void LLFloaterInventoryThumbnailsHelper::updateDisplayList()
 {
-    std::map<std::string, LLUUID>::iterator item_iter = mItemNamesIDs.begin();
+    mInventoryThumbnailsList->deleteAllItems();
 
-    while (item_iter != mItemNamesIDs.end())
+    std::map<std::string, LLViewerInventoryItem*>::iterator item_iter = mItemNamesItems.begin();
+    while (item_iter != mItemNamesItems.end())
     {
         std::string item_name = (*item_iter).first;
 
-        std::map<std::string, LLUUID>::iterator texture_iter = mTextureNamesIDs.find(item_name);
-        if (texture_iter != mTextureNamesIDs.end())
+        std::string existing_texture_name = std::string();
+        LLUUID existing_thumbnail_id = (*item_iter).second->getThumbnailUUID();
+        if (existing_thumbnail_id != LLUUID::null)
         {
-            const bool case_sensitive = true;
-            LLScrollListItem* entry = mInventoryThumbnailsList->getItemByLabel(item_name, case_sensitive);
-
-            const std::string texture_name = (*texture_iter).first;
-            entry->getColumn(1)->setValue(LLSD(texture_name));
+            existing_texture_name = existing_thumbnail_id.asString();
+        }
+        else
+        {
+            existing_texture_name = "none";
         }
 
-        ++item_iter;
-    }
-}
-
-void LLFloaterInventoryThumbnailsHelper::mergeItemsTextures()
-{
-    mOutputLog->appendText(
-        STRINGIZE(
-            "\n==== Matching items and textures for " <<
-            mItemNamesIDs.size() <<
-            " entries ====" <<
-            std::endl
-        ), false);
-
-    std::map<std::string, LLUUID>::iterator item_iter = mItemNamesIDs.begin();
-    size_t index = 1;
-
-    while (item_iter != mItemNamesIDs.end())
-    {
-        std::string item_name = (*item_iter).first;
-
-        mOutputLog->appendText(
-            STRINGIZE(
-                "MATCHING ITEM (" << index++  << "/" << mItemNamesIDs.size() << ") " << item_name << "> "
-            ), false);
-
+        std::string new_texture_name = std::string();
         std::map<std::string, LLUUID>::iterator texture_iter = mTextureNamesIDs.find(item_name);
         if (texture_iter != mTextureNamesIDs.end())
         {
-            mOutputLog->appendText(
-                STRINGIZE(
-                    "MATCHED" <<
-                    std::endl
-                ), false);
-
-            mNameItemIDTextureId.insert({item_name, {(*item_iter).second, (*texture_iter).second}});
+            new_texture_name = (*texture_iter).first;
         }
         else
         {
-            mOutputLog->appendText(
-                STRINGIZE(
-                    "NO MATCH FOUND" <<
-                    std::endl
-                ), false);
+            new_texture_name = "missing";
         }
 
-        ++item_iter;
-    }
+        LLSD row;
+        row["columns"][EListColumnNum::NAME]["column"] = "item_name";
+        row["columns"][EListColumnNum::NAME]["type"] = "text";
+        row["columns"][EListColumnNum::NAME]["value"] = item_name;
+        row["columns"][EListColumnNum::NAME]["font"]["name"] = "Monospace";
 
-    mOutputLog->appendText(
-        STRINGIZE(
-            "==== Matched list of items and textures has " <<
-            mNameItemIDTextureId.size() <<
-            " entries ====" <<
-            std::endl
-        ), true);
-
-    //std::map<std::string, std::pair< LLUUID, LLUUID>>::iterator iter = mNameItemIDTextureId.begin();
-    //while (iter != mNameItemIDTextureId.end())
-    //{
-    //    std::string output_line = (*iter).first;
-    //    output_line += "\n";
-    //    output_line += "item ID: ";
-    //    output_line += ((*iter).second).first.asString();
-    //    output_line += "\n";
-    //    output_line += "thumbnail texture ID: ";
-    //    output_line += ((*iter).second).second.asString();
-    //    output_line +=  "\n";
-    //    mOutputLog->appendText(output_line, true);
-
-    //    ++iter;
-    //}
-    mOutputLog->setCursorAndScrollToEnd();
+        row["columns"][EListColumnNum::EXISTING_TEXTURE]["column"] = "existing_texture";
+        row["columns"][EListColumnNum::EXISTING_TEXTURE]["type"] = "text";
+        row["columns"][EListColumnNum::EXISTING_TEXTURE]["font"]["name"] = "Monospace";
+        row["columns"][EListColumnNum::EXISTING_TEXTURE]["value"] = existing_texture_name;
+
+        row["columns"][EListColumnNum::NEW_TEXTURE]["column"] = "new_texture";
+        row["columns"][EListColumnNum::NEW_TEXTURE]["type"] = "text";
+        row["columns"][EListColumnNum::NEW_TEXTURE]["font"]["name"] = "Monospace";
+        row["columns"][EListColumnNum::NEW_TEXTURE]["value"] = new_texture_name;
+
+        mInventoryThumbnailsList->addElement(row);
 
-    mWriteThumbnailsBtn->setEnabled(true);
+        ++item_iter;
+    }
 }
 
 #if 1
@@ -373,6 +342,9 @@ void inventoryThumbnailsHelperCb(LLPointer<LLInventoryCallback> cb, LLUUID id)
 }
 #endif
 
+// Makes calls to the AIS v3 API to record the local changes made to the thumbnails.
+// If this is not called, the operations (e.g. set thumbnail or clear thumbnail)
+// appear to work but do not push the changes back to the inventory (local cache view only)
 bool writeInventoryThumbnailID(LLUUID item_id, LLUUID thumbnail_asset_id)
 {
     if (AISAPI::isAvailable())
@@ -395,40 +367,164 @@ bool writeInventoryThumbnailID(LLUUID item_id, LLUUID thumbnail_asset_id)
     }
 }
 
+// Called when the Write Thumbanils button is pushed. Iterates over the name/item and
+// name/.texture maps and where it finds a common name, extracts what is needed and
+// writes the thumbnail accordingly.
 void LLFloaterInventoryThumbnailsHelper::onWriteThumbnails()
 {
-    mOutputLog->appendText(
-        STRINGIZE(
-            "\n==== Writing thumbnails for " <<
-            mNameItemIDTextureId.size() <<
-            " entries ====" <<
-            std::endl
-        ), false);
+    std::map<std::string, LLViewerInventoryItem*>::iterator item_iter = mItemNamesItems.begin();
+    while (item_iter != mItemNamesItems.end())
+    {
+        std::string item_name = (*item_iter).first;
+
+        std::map<std::string, LLUUID>::iterator texture_iter = mTextureNamesIDs.find(item_name);
+        if (texture_iter != mTextureNamesIDs.end())
+        {
+            LLUUID item_id = (*item_iter).second->getUUID();
+
+            LLUUID thumbnail_asset_id = (*texture_iter).second;
 
-    std::map<std::string, std::pair< LLUUID, LLUUID>>::iterator iter = mNameItemIDTextureId.begin();
-    size_t index = 1;
+            writeToLog(
+                STRINGIZE(
+                    "WRITING THUMB " <<
+                    (*item_iter).first <<
+                    "\n" <<
+                    "item ID: " <<
+                    item_id <<
+                    "\n" <<
+                    "thumbnail texture ID: " <<
+                    thumbnail_asset_id <<
+                    "\n"
+                ), true);
+
+
+            (*item_iter).second->setThumbnailUUID(thumbnail_asset_id);
+
+            // This additional step (notifying AIS API) is required
+            // to make the changes persist outside of the local cache
+            writeInventoryThumbnailID(item_id, thumbnail_asset_id);
+        }
 
-    while (iter != mNameItemIDTextureId.end())
+        ++item_iter;
+    }
+
+    updateDisplayList();
+}
+
+// Called when the Log Items with Missing Thumbnails is selected. This merely writes
+// a list of all the items for which the thumbnail ID is Null. Typical use case is to
+// copy from the log window, pasted to Slack to illustrate which items are missing
+// a thumbnail
+void LLFloaterInventoryThumbnailsHelper::onLogMissingThumbnails()
+{
+    std::map<std::string, LLViewerInventoryItem*>::iterator item_iter = mItemNamesItems.begin();
+    while (item_iter != mItemNamesItems.end())
     {
-        mOutputLog->appendText(
-            STRINGIZE(
-                "WRITING THUMB (" << index++  << "/" << mNameItemIDTextureId.size() << ")> " <<
-                (*iter).first <<
-                "\n" <<
-                "item ID: " <<
-                ((*iter).second).first.asString() <<
-                "\n" <<
-                "thumbnail texture ID: " <<
-                ((*iter).second).second.asString() <<
-                "\n"
-            ), true);
-
-        LLUUID item_id = ((*iter).second).first;
-        LLUUID thumbnail_asset_id = ((*iter).second).second;
-
-        writeInventoryThumbnailID(item_id, thumbnail_asset_id);
-
-        ++iter;
+        LLUUID thumbnail_id = (*item_iter).second->getThumbnailUUID();
+
+        if (thumbnail_id == LLUUID::null)
+        {
+            writeToLog(
+                STRINGIZE(
+                    "Missing thumbnail: " <<
+                    (*item_iter).first <<
+                    std::endl
+                ), true);
+        }
+
+        ++item_iter;
+    }
+}
+
+// Called when the Clear Thumbnail button is selected.  Code to perform the clear (really
+// just writing a NULL UUID into the thumbnail field) is behind an "Are you Sure?" dialog
+// since it cannot be undone and potentinally, you could remove the thumbnails from your
+// whole inventory this way.
+void LLFloaterInventoryThumbnailsHelper::onClearThumbnails()
+{
+    // create and show confirmation (Yes/No) textbox since this is a destructive operation
+    LLNotificationsUtil::add("ClearInventoryThumbnailsWarning", LLSD(), LLSD(),
+                             [&](const LLSD & notif, const LLSD & resp)
+    {
+        S32 opt = LLNotificationsUtil::getSelectedOption(notif, resp);
+        if (opt == 0)
+        {
+            std::map<std::string, LLViewerInventoryItem*>::iterator item_iter = mItemNamesItems.begin();
+            while (item_iter != mItemNamesItems.end())
+            {
+                (*item_iter).second->setThumbnailUUID(LLUUID::null);
+
+                // This additional step (notifying AIS API) is required
+                // to make the changes persist outside of the local cache
+                const LLUUID item_id = (*item_iter).second->getUUID();
+                writeInventoryThumbnailID(item_id, LLUUID::null);
+
+                ++item_iter;
+            }
+
+            updateDisplayList();
+        }
+        else
+        {
+            LL_INFOS() << "Clearing on thumbnails was canceled" << LL_ENDL;
+        }
+    });
+}
+
+// Update the endabled state of some of the UI buttons based on what has
+// been recorded so far.  For example, if there are no valid item/texture pairs,
+// then the Write Thumbnails button is not enabled.
+void LLFloaterInventoryThumbnailsHelper::updateButtonStates()
+{
+    size_t found_count = 0;
+
+    std::map<std::string, LLViewerInventoryItem*>::iterator item_iter = mItemNamesItems.begin();
+    while (item_iter != mItemNamesItems.end())
+    {
+        std::string item_name = (*item_iter).first;
+
+        std::map<std::string, LLUUID>::iterator texture_iter = mTextureNamesIDs.find(item_name);
+        if (texture_iter != mTextureNamesIDs.end())
+        {
+            found_count++;
+        }
+
+        ++item_iter;
+    }
+
+    // the "Write Thumbnails" button is only enabled when there is at least one
+    // item with a matching texture ready to be written to the thumbnail field
+    if (found_count > 0)
+    {
+        mWriteThumbnailsBtn->setEnabled(true);
     }
+    else
+    {
+        mWriteThumbnailsBtn->setEnabled(false);
+    }
+
+    // The "Log Missing Items" and "Clear Thumbnails" buttons are only enabled
+    // when there is at least 1 item that was pasted from inventory (doesn't need
+    // to have a matching texture for these operations)
+    if (mItemNamesItems.size() > 0)
+    {
+        mLogMissingThumbnailsBtn->setEnabled(true);
+        mClearThumbnailsBtn->setEnabled(true);
+    }
+    else
+    {
+        mLogMissingThumbnailsBtn->setEnabled(false);
+        mClearThumbnailsBtn->setEnabled(false);
+    }
+}
+
+// Helper function for writing a line to the log window. Currently the only additional
+// feature is that it scrolls to the bottom each time a line is written but it
+// is envisaged that other common actions will be added here eventually - E.G. write eavh
+// line to the Second Life log too for example.
+void LLFloaterInventoryThumbnailsHelper::writeToLog(std::string logline, bool prepend_newline)
+{
+    mOutputLog->appendText(logline, prepend_newline);
+
     mOutputLog->setCursorAndScrollToEnd();
 }
diff --git a/indra/newview/llfloaterinventorythumbnailshelper.h b/indra/newview/llfloaterinventorythumbnailshelper.h
index ec83d5b7e06..b42a85d1a56 100644
--- a/indra/newview/llfloaterinventorythumbnailshelper.h
+++ b/indra/newview/llfloaterinventorythumbnailshelper.h
@@ -1,7 +1,7 @@
 /**
  * @file llfloaterinventorythumbnailshelper.h
  * @author Callum Prentice
- * @brief Helper floater for bulk processing of inventory thumbnails
+ * @brief Helper floater for bulk processing of inventory thumbnails tool
  *
  * $LicenseInfo:firstyear=2008&license=viewerlgpl$
  * Second Life Viewer Source Code
@@ -31,7 +31,6 @@
 #include "llfloater.h"
 class LLTextEditor;
 class LLScrollListCtrl;
-class LLMediaCtrl;
 class LLViewerInventoryItem;
 class LLUUID;
 
@@ -46,27 +45,38 @@ class LLFloaterInventoryThumbnailsHelper:
 
         LLScrollListCtrl* mInventoryThumbnailsList;
 
+        LLTextEditor* mOutputLog;
+
         LLUICtrl* mPasteItemsBtn;
         void onPasteItems();
 
         LLUICtrl* mPasteTexturesBtn;
         void onPasteTextures();
 
-        LLTextEditor* mOutputLog;
-
-        void mergeItemsTextures();
-
         LLUICtrl* mWriteThumbnailsBtn;
         void onWriteThumbnails();
 
+        LLUICtrl* mLogMissingThumbnailsBtn;
+        void onLogMissingThumbnails();
+
+        LLUICtrl* mClearThumbnailsBtn;
+        void onClearThumbnails();
+
         void recordInventoryItemEntry(LLViewerInventoryItem* item);
         void recordTextureItemEntry(LLViewerInventoryItem* item);
-        void populateThumbnailNames();
+        void updateButtonStates();
+        void updateDisplayList();
+        void writeToLog(std::string logline, bool prepend_newline);
 
-        std::map<std::string, LLUUID> mItemNamesIDs;
+        std::map<std::string, LLViewerInventoryItem*> mItemNamesItems;
         std::map<std::string, LLUUID> mTextureNamesIDs;
 
-        std::map<std::string, std::pair< LLUUID, LLUUID>> mNameItemIDTextureId;
+        enum EListColumnNum
+        {
+            NAME = 0,
+            EXISTING_TEXTURE = 1,
+            NEW_TEXTURE = 2
+        };
 };
 
 #endif // LL_LLFLOATERINVENTORYTHUMBNAILSHELPER_H
diff --git a/indra/newview/skins/default/xui/en/floater_bulky_thumbs.xml b/indra/newview/skins/default/xui/en/floater_bulky_thumbs.xml
index e94717f9470..d23fd1247f3 100644
--- a/indra/newview/skins/default/xui/en/floater_bulky_thumbs.xml
+++ b/indra/newview/skins/default/xui/en/floater_bulky_thumbs.xml
@@ -44,14 +44,23 @@
       left="10"
       name="merge_items_textures"
       bottom="8"
-      width="200" />
+      width="100" />
     <button
       follows="left|bottom"
       height="20"
       label="Write Thumbnails"
       layout="bottomleft"
-      left="250"
+      left="150"
       name="write_items_thumbnails"
       bottom="8"
-      width="200" />
+      width="100" />
+    <button
+      follows="left|bottom"
+      height="20"
+      label="Missing Thumbnails"
+      layout="bottomleft"
+      left="250"
+      name="display_thumbless_items"
+      bottom="8"
+      width="100" />
 </floater>
\ No newline at end of file
diff --git a/indra/newview/skins/default/xui/en/floater_inventory_thumbnails_helper.xml b/indra/newview/skins/default/xui/en/floater_inventory_thumbnails_helper.xml
index 512bce2475d..aa3500bac29 100644
--- a/indra/newview/skins/default/xui/en/floater_inventory_thumbnails_helper.xml
+++ b/indra/newview/skins/default/xui/en/floater_inventory_thumbnails_helper.xml
@@ -9,7 +9,7 @@
   name="contents"
   help_topic="contents"
   title="Inventory Thumbnails Helper"
-  width="500">
+  width="800">
     <scroll_list
        top="20"
        height="350"
@@ -23,14 +23,20 @@
        right="-8"
        tool_tip="Paste items from your inventory">
         <scroll_list.columns
+             dynamic_width="true"
              label="Inventory Item"
-             name="name"
-             relative_width="0.40" />
+             name="item_name"
+             relative_width="0.4" />
+        <scroll_list.columns
+             dynamic_width="true"
+             label="Existing Texture"
+             name="existing_texture"
+             relative_width="0.3" />
         <scroll_list.columns
              dynamic_width="true"
-             label="Texture Name"
-             name="texture"
-             relative_width="0.6" />
+             label="New Texture"
+             name="new_texture"
+             relative_width="0.3" />
     </scroll_list>
     <text_editor
       top="375"
@@ -73,10 +79,21 @@
     <button
       follows="left|bottom"
       height="20"
-      label="List items with no thumb"
+      label="Log items with no thumbnail"
       layout="bottomleft"
       right="-10"
-      name="list_items_no_thumb_btn"
+      name="log_missing_thumbnails_btn"
       bottom="60"
       width="235" />
+    <button
+      follows="left|bottom"
+      height="20"
+      label="Clear thumbnails from pasted items"
+      layout="bottomleft"
+      right="-10"
+      name="clear_thumbnails_btn"
+      top_delta="26"
+      width="235" />
+
+
 </floater>
\ No newline at end of file
diff --git a/indra/newview/skins/default/xui/en/notifications.xml b/indra/newview/skins/default/xui/en/notifications.xml
index ef720e65e38..45020163ef9 100644
--- a/indra/newview/skins/default/xui/en/notifications.xml
+++ b/indra/newview/skins/default/xui/en/notifications.xml
@@ -12074,4 +12074,18 @@ Would you like to save them first?
        yestext="Yes"/>
   </notification>
     
+    <notification
+  icon="alertmodal.tga"
+  name="ClearInventoryThumbnailsWarning"
+  type="alertmodal">
+        You are about to remove thumbnail images from the inventory items in the list. This change cannot be undone.
+
+        Would you like to proceed?
+        <tag>confirm</tag>
+        <usetemplate
+         name="okcancelbuttons"
+         notext="No"
+         yestext="Yes"/>
+    </notification>
+    
 </notifications>
-- 
GitLab