/** * @file llfloaterchangeitemthumbnail.cpp * @brief LLFloaterChangeItemThumbnail class implementation * * $LicenseInfo:firstyear=2023&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2023, 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 "llfloaterchangeitemthumbnail.h" #include "llbutton.h" #include "llclipboard.h" #include "lliconctrl.h" #include "llinventoryfunctions.h" #include "llinventoryicon.h" #include "llinventorymodel.h" #include "llinventoryobserver.h" #include "llfloaterreg.h" #include "llfloatersimplesnapshot.h" #include "lllineeditor.h" #include "llnotificationsutil.h" #include "lltextbox.h" #include "lltexturectrl.h" #include "llthumbnailctrl.h" #include "llviewerfoldertype.h" #include "llviewermenufile.h" #include "llviewerobjectlist.h" #include "llviewertexturelist.h" #include "llwindow.h" class LLThumbnailImagePicker : public LLFilePickerThread { public: LLThumbnailImagePicker(const LLUUID &item_id); LLThumbnailImagePicker(const LLUUID &item_id, const LLUUID &task_id); ~LLThumbnailImagePicker(); void notify(const std::vector& filenames) override; private: LLUUID mInventoryId; LLUUID mTaskId; }; LLThumbnailImagePicker::LLThumbnailImagePicker(const LLUUID &item_id) : LLFilePickerThread(LLFilePicker::FFLOAD_IMAGE) , mInventoryId(item_id) { } LLThumbnailImagePicker::LLThumbnailImagePicker(const LLUUID &item_id, const LLUUID &task_id) : LLFilePickerThread(LLFilePicker::FFLOAD_IMAGE) , mInventoryId(item_id) , mTaskId(task_id) { } LLThumbnailImagePicker::~LLThumbnailImagePicker() { } void LLThumbnailImagePicker::notify(const std::vector& filenames) { if (filenames.empty()) { return; } std::string file_path = filenames[0]; if (file_path.empty()) { return; } LLFloaterSimpleSnapshot::uploadThumbnail(file_path, mInventoryId, mTaskId); } LLFloaterChangeItemThumbnail::LLFloaterChangeItemThumbnail(const LLSD& key) : LLFloater(key) , mObserverInitialized(false) , mTooltipState(TOOLTIP_NONE) { } LLFloaterChangeItemThumbnail::~LLFloaterChangeItemThumbnail() { gInventory.removeObserver(this); removeVOInventoryListener(); } BOOL LLFloaterChangeItemThumbnail::postBuild() { mItemNameText = getChild("item_name"); mItemTypeIcon = getChild("item_type_icon"); mThumbnailCtrl = getChild("item_thumbnail"); mToolTipTextBox = getChild("tooltip_text"); LLSD tooltip_text; mToolTipTextBox->setValue(tooltip_text); LLButton *upload_local = getChild("upload_local"); upload_local->setClickedCallback(onUploadLocal, (void*)this); upload_local->setMouseEnterCallback(boost::bind(&LLFloaterChangeItemThumbnail::onButtonMouseEnter, this, _1, _2, TOOLTIP_UPLOAD_LOCAL)); upload_local->setMouseLeaveCallback(boost::bind(&LLFloaterChangeItemThumbnail::onButtonMouseLeave, this, _1, _2, TOOLTIP_UPLOAD_LOCAL)); LLButton *upload_snapshot = getChild("upload_snapshot"); upload_snapshot->setClickedCallback(onUploadSnapshot, (void*)this); upload_snapshot->setMouseEnterCallback(boost::bind(&LLFloaterChangeItemThumbnail::onButtonMouseEnter, this, _1, _2, TOOLTIP_UPLOAD_SNAPSHOT)); upload_snapshot->setMouseLeaveCallback(boost::bind(&LLFloaterChangeItemThumbnail::onButtonMouseLeave, this, _1, _2, TOOLTIP_UPLOAD_SNAPSHOT)); LLButton *use_texture = getChild("use_texture"); use_texture->setClickedCallback(onUseTexture, (void*)this); use_texture->setMouseEnterCallback(boost::bind(&LLFloaterChangeItemThumbnail::onButtonMouseEnter, this, _1, _2, TOOLTIP_USE_TEXTURE)); use_texture->setMouseLeaveCallback(boost::bind(&LLFloaterChangeItemThumbnail::onButtonMouseLeave, this, _1, _2, TOOLTIP_USE_TEXTURE)); mCopyToClipboardBtn = getChild("copy_to_clipboard"); mCopyToClipboardBtn->setClickedCallback(onCopyToClipboard, (void*)this); mCopyToClipboardBtn->setMouseEnterCallback(boost::bind(&LLFloaterChangeItemThumbnail::onButtonMouseEnter, this, _1, _2, TOOLTIP_COPY_TO_CLIPBOARD)); mCopyToClipboardBtn->setMouseLeaveCallback(boost::bind(&LLFloaterChangeItemThumbnail::onButtonMouseLeave, this, _1, _2, TOOLTIP_COPY_TO_CLIPBOARD)); mPasteFromClipboardBtn = getChild("paste_from_clipboard"); mPasteFromClipboardBtn->setClickedCallback(onPasteFromClipboard, (void*)this); mPasteFromClipboardBtn->setMouseEnterCallback(boost::bind(&LLFloaterChangeItemThumbnail::onButtonMouseEnter, this, _1, _2, TOOLTIP_COPY_FROM_CLIPBOARD)); mPasteFromClipboardBtn->setMouseLeaveCallback(boost::bind(&LLFloaterChangeItemThumbnail::onButtonMouseLeave, this, _1, _2, TOOLTIP_COPY_FROM_CLIPBOARD)); mRemoveImageBtn = getChild("remove_image"); mRemoveImageBtn->setClickedCallback(onRemove, (void*)this); mRemoveImageBtn->setMouseEnterCallback(boost::bind(&LLFloaterChangeItemThumbnail::onButtonMouseEnter, this, _1, _2, TOOLTIP_REMOVE)); mRemoveImageBtn->setMouseLeaveCallback(boost::bind(&LLFloaterChangeItemThumbnail::onButtonMouseLeave, this, _1, _2, TOOLTIP_REMOVE)); return LLFloater::postBuild(); } void LLFloaterChangeItemThumbnail::onOpen(const LLSD& key) { if (!key.has("item_id") && !key.isUUID()) { closeFloater(); } if (key.isUUID()) { mItemId = key.asUUID(); } else { mItemId = key["item_id"].asUUID(); mTaskId = key["task_id"].asUUID(); } refreshFromInventory(); } void LLFloaterChangeItemThumbnail::onFocusReceived() { mPasteFromClipboardBtn->setEnabled(LLClipboard::instance().hasContents()); } void LLFloaterChangeItemThumbnail::onMouseEnter(S32 x, S32 y, MASK mask) { mPasteFromClipboardBtn->setEnabled(LLClipboard::instance().hasContents()); } BOOL LLFloaterChangeItemThumbnail::handleDragAndDrop( S32 x, S32 y, MASK mask, BOOL drop, EDragAndDropType cargo_type, void *cargo_data, EAcceptance *accept, std::string& tooltip_msg) { if (cargo_type == DAD_TEXTURE) { LLInventoryItem *item = (LLInventoryItem *)cargo_data; if (item->getAssetUUID().notNull()) { if (drop) { assignAndValidateAsset(item->getAssetUUID()); } *accept = ACCEPT_YES_SINGLE; } else { *accept = ACCEPT_NO; } } else { *accept = ACCEPT_NO; } LL_DEBUGS("UserInput") << "dragAndDrop handled by LLFloaterChangeItemThumbnail " << getKey() << LL_ENDL; return TRUE; } void LLFloaterChangeItemThumbnail::changed(U32 mask) { //LLInventoryObserver if (mTaskId.notNull() || mItemId.isNull()) { // Task inventory or not set up yet return; } const std::set& mChangedItemIDs = gInventory.getChangedIDs(); std::set::const_iterator it; for (it = mChangedItemIDs.begin(); it != mChangedItemIDs.end(); it++) { // set dirty for 'item profile panel' only if changed item is the item for which 'item profile panel' is shown (STORM-288) if (*it == mItemId) { // if there's a change we're interested in. if ((mask & (LLInventoryObserver::LABEL | LLInventoryObserver::INTERNAL | LLInventoryObserver::REMOVE)) != 0) { refreshFromInventory(); } } } } void LLFloaterChangeItemThumbnail::inventoryChanged(LLViewerObject* object, LLInventoryObject::object_list_t* inventory, S32 serial_num, void* user_data) { //LLVOInventoryListener refreshFromInventory(); } LLInventoryObject* LLFloaterChangeItemThumbnail::getInventoryObject() { LLInventoryObject* obj = NULL; if (mTaskId.isNull()) { // it is in agent inventory if (!mObserverInitialized) { gInventory.addObserver(this); mObserverInitialized = true; } obj = gInventory.getObject(mItemId); } else { LLViewerObject* object = gObjectList.findObject(mTaskId); if (object) { if (!mObserverInitialized) { registerVOInventoryListener(object, NULL); mObserverInitialized = false; } obj = object->getInventoryObject(mItemId); } } return obj; } void LLFloaterChangeItemThumbnail::refreshFromInventory() { LLInventoryObject* obj = getInventoryObject(); if (!obj) { closeFloater(); } if (obj) { const LLUUID trash_id = gInventory.findCategoryUUIDForType(LLFolderType::FT_TRASH); bool in_trash = gInventory.isObjectDescendentOf(obj->getUUID(), trash_id); if (in_trash && obj->getUUID() != trash_id) { // Close properties when moving to trash // Aren't supposed to view properties from trash closeFloater(); } else { refreshFromObject(obj); } } else { closeFloater(); } } class LLIsOutfitTextureType : public LLInventoryCollectFunctor { public: LLIsOutfitTextureType() {} virtual ~LLIsOutfitTextureType() {} virtual bool operator()(LLInventoryCategory* cat, LLInventoryItem* item); }; bool LLIsOutfitTextureType::operator()(LLInventoryCategory* cat, LLInventoryItem* item) { return item && (item->getType() == LLAssetType::AT_TEXTURE); } void LLFloaterChangeItemThumbnail::refreshFromObject(LLInventoryObject* obj) { LLUIImagePtr icon_img; LLUUID thumbnail_id = obj->getThumbnailUUID(); LLViewerInventoryItem* item = dynamic_cast(obj); if (item) { setTitle(getString("title_item_thumbnail")); icon_img = LLInventoryIcon::getIcon(item->getType(), item->getInventoryType(), item->getFlags(), FALSE); mRemoveImageBtn->setEnabled(thumbnail_id.notNull() && ((item->getActualType() != LLAssetType::AT_TEXTURE) || (item->getAssetUUID() != thumbnail_id))); } else { LLViewerInventoryCategory* cat = dynamic_cast(obj); if (cat) { setTitle(getString("title_folder_thumbnail")); icon_img = LLUI::getUIImage(LLViewerFolderType::lookupIconName(cat->getPreferredType(), true)); if (thumbnail_id.isNull() && (cat->getPreferredType() == LLFolderType::FT_OUTFIT)) { // Legacy support, check if there is an image inside LLInventoryModel::cat_array_t cats; LLInventoryModel::item_array_t items; // Not LLIsOfAssetType, because we allow links LLIsOutfitTextureType f; gInventory.getDirectDescendentsOf(mItemId, cats, items, f); if (1 == items.size()) { LLViewerInventoryItem* item = items.front(); if (item && item->getIsLinkType()) { item = item->getLinkedItem(); } if (item) { thumbnail_id = item->getAssetUUID(); if (thumbnail_id.notNull()) { // per SL-19188, set this image as a thumbnail LL_INFOS() << "Setting image " << thumbnail_id << " from outfit as a thumbnail for inventory object " << obj->getUUID() << LL_ENDL; assignAndValidateAsset(thumbnail_id, true); } } } } mRemoveImageBtn->setEnabled(thumbnail_id.notNull()); } } mItemTypeIcon->setImage(icon_img); mItemNameText->setValue(obj->getName()); mThumbnailCtrl->setValue(thumbnail_id); mCopyToClipboardBtn->setEnabled(thumbnail_id.notNull()); mPasteFromClipboardBtn->setEnabled(LLClipboard::instance().hasContents()); // todo: some elements might not support setting thumbnails // since they already have them // It is unclear how system folders should function } void LLFloaterChangeItemThumbnail::onUploadLocal(void *userdata) { LLFloaterChangeItemThumbnail *self = (LLFloaterChangeItemThumbnail*)userdata; (new LLThumbnailImagePicker(self->mItemId, self->mTaskId))->getFile(); LLFloater* floaterp = self->mPickerHandle.get(); if (floaterp) { floaterp->closeFloater(); } floaterp = self->mSnapshotHandle.get(); if (floaterp) { floaterp->closeFloater(); } } void LLFloaterChangeItemThumbnail::onUploadSnapshot(void *userdata) { LLFloaterChangeItemThumbnail *self = (LLFloaterChangeItemThumbnail*)userdata; LLFloater* floaterp = self->mSnapshotHandle.get(); // Show the dialog if (floaterp) { floaterp->openFloater(); } else { LLSD key; key["item_id"] = self->mItemId; key["task_id"] = self->mTaskId; LLFloaterSimpleSnapshot* snapshot_floater = (LLFloaterSimpleSnapshot*)LLFloaterReg::showInstance("simple_snapshot", key, true); if (snapshot_floater) { self->addDependentFloater(snapshot_floater); self->mSnapshotHandle = snapshot_floater->getHandle(); snapshot_floater->setOwner(self); } } floaterp = self->mPickerHandle.get(); if (floaterp) { floaterp->closeFloater(); } } void LLFloaterChangeItemThumbnail::onUseTexture(void *userdata) { LLFloaterChangeItemThumbnail *self = (LLFloaterChangeItemThumbnail*)userdata; LLInventoryObject* obj = self->getInventoryObject(); if (obj) { self->showTexturePicker(obj->getThumbnailUUID()); } LLFloater* floaterp = self->mSnapshotHandle.get(); if (floaterp) { floaterp->closeFloater(); } } void LLFloaterChangeItemThumbnail::onCopyToClipboard(void *userdata) { LLFloaterChangeItemThumbnail *self = (LLFloaterChangeItemThumbnail*)userdata; LLInventoryObject* obj = self->getInventoryObject(); if (obj) { LLClipboard::instance().reset(); LLClipboard::instance().addToClipboard(obj->getThumbnailUUID(), LLAssetType::AT_NONE); self->mPasteFromClipboardBtn->setEnabled(true); } } void LLFloaterChangeItemThumbnail::onPasteFromClipboard(void *userdata) { LLFloaterChangeItemThumbnail *self = (LLFloaterChangeItemThumbnail*)userdata; std::vector objects; LLClipboard::instance().pasteFromClipboard(objects); if (objects.size() > 0) { LLUUID potential_uuid = objects[0]; LLUUID asset_id; if (potential_uuid.notNull()) { LLViewerInventoryItem* item = gInventory.getItem(potential_uuid); if (item) { // no point checking snapshot? if (item->getType() == LLAssetType::AT_TEXTURE) { bool copy = item->getPermissions().allowCopyBy(gAgent.getID()); bool xfer = item->getPermissions().allowOperationBy(PERM_TRANSFER, gAgent.getID()); if (copy && xfer) { asset_id = item->getAssetUUID(); } else { LLNotificationsUtil::add("ThumbnailInsufficientPermissions"); return; } } } else { // assume that this is a texture asset_id = potential_uuid; } } if (asset_id.notNull()) { self->assignAndValidateAsset(asset_id); } // else show 'buffer has no texture' warning? } } void LLFloaterChangeItemThumbnail::onRemove(void *userdata) { LLFloaterChangeItemThumbnail *self = (LLFloaterChangeItemThumbnail*)userdata; LLSD payload; payload["item_id"] = self->mItemId; payload["object_id"] = self->mTaskId; LLNotificationsUtil::add("DeleteThumbnail", LLSD(), payload, boost::bind(&LLFloaterChangeItemThumbnail::onRemovalConfirmation, _1, _2, self->getHandle())); } // static void LLFloaterChangeItemThumbnail::onRemovalConfirmation(const LLSD& notification, const LLSD& response, LLHandle handle) { S32 option = LLNotificationsUtil::getSelectedOption(notification, response); if (option == 0 && !handle.isDead() && !handle.get()->isDead()) { LLFloaterChangeItemThumbnail* self = (LLFloaterChangeItemThumbnail*)handle.get(); self->setThumbnailId(LLUUID::null); } } struct ImageLoadedData { LLUUID mThumbnailId; LLUUID mObjectId; LLHandle mFloaterHandle; }; void LLFloaterChangeItemThumbnail::assignAndValidateAsset(const LLUUID &asset_id, bool silent) { LLPointer texturep = LLViewerTextureManager::getFetchedTexture(asset_id); if (texturep->getFullWidth() == 0 && !texturep->isFullyLoaded() && !texturep->isMissingAsset()) { if (silent) { mExpectingAssetId = LLUUID::null; } else { // don't warn user multiple times if some textures took their time mExpectingAssetId = asset_id; } ImageLoadedData *data = new ImageLoadedData(); data->mObjectId = mItemId; data->mThumbnailId = asset_id; data->mFloaterHandle = getHandle(); texturep->setLoadedCallback(onImageLoaded, MAX_DISCARD_LEVEL, // don't actually need max one, 3 or 4 should be enough FALSE, FALSE, (void*)data, NULL, FALSE); } else { if (validateAsset(asset_id)) { setThumbnailId(asset_id); } else if (!silent) { LLNotificationsUtil::add("ThumbnailDimentionsLimit"); } } } bool LLFloaterChangeItemThumbnail::validateAsset(const LLUUID &asset_id) { if (asset_id.isNull()) { return false; } LLPointer texturep = LLViewerTextureManager::findFetchedTexture(asset_id, TEX_LIST_STANDARD); if (!texturep) { return false; } if (texturep->isMissingAsset()) { return false; } if (texturep->getFullWidth() != texturep->getFullHeight()) { return false; } if (texturep->getFullWidth() > LLFloaterSimpleSnapshot::THUMBNAIL_SNAPSHOT_DIM_MAX || texturep->getFullHeight() > LLFloaterSimpleSnapshot::THUMBNAIL_SNAPSHOT_DIM_MAX) { return false; } if (texturep->getFullWidth() < LLFloaterSimpleSnapshot::THUMBNAIL_SNAPSHOT_DIM_MIN || texturep->getFullHeight() < LLFloaterSimpleSnapshot::THUMBNAIL_SNAPSHOT_DIM_MIN) { return false; } return true; } //static void LLFloaterChangeItemThumbnail::onImageLoaded( BOOL success, LLViewerFetchedTexture *src_vi, LLImageRaw* src, LLImageRaw* aux_src, S32 discard_level, BOOL final, void* userdata) { if (!userdata) return; if (!final && success) return; //not done yet ImageLoadedData* data = (ImageLoadedData*)userdata; if (success) { // Update the item, set it even if floater is dead if (validateAsset(data->mThumbnailId)) { setThumbnailId(data->mThumbnailId, data->mObjectId); } // Update floater if (!data->mFloaterHandle.isDead()) { LLFloaterChangeItemThumbnail* self = static_cast(data->mFloaterHandle.get()); if (self && self->mExpectingAssetId == data->mThumbnailId) { LLNotificationsUtil::add("ThumbnailDimentionsLimit"); self->mExpectingAssetId = LLUUID::null; } } } delete data; } void LLFloaterChangeItemThumbnail::showTexturePicker(const LLUUID &thumbnail_id) { // show hourglass cursor when loading inventory window getWindow()->setCursor(UI_CURSOR_WAIT); LLFloater* floaterp = mPickerHandle.get(); // Show the dialog if (floaterp) { floaterp->openFloater(); } else { floaterp = new LLFloaterTexturePicker( this, thumbnail_id, thumbnail_id, thumbnail_id, FALSE, TRUE, "SELECT PHOTO", PERM_NONE, PERM_NONE, PERM_NONE, FALSE, NULL); mPickerHandle = floaterp->getHandle(); LLFloaterTexturePicker* texture_floaterp = dynamic_cast(floaterp); if (texture_floaterp) { //texture_floaterp->setTextureSelectedCallback(); //texture_floaterp->setOnUpdateImageStatsCallback(); texture_floaterp->setOnFloaterCommitCallback([this](LLTextureCtrl::ETexturePickOp op, LLUUID id) { if (op == LLTextureCtrl::TEXTURE_SELECT) { onTexturePickerCommit(id); } } ); texture_floaterp->setLocalTextureEnabled(FALSE); texture_floaterp->setBakeTextureEnabled(FALSE); texture_floaterp->setCanApplyImmediately(false); texture_floaterp->setCanApply(false, true, false /*Hide 'preview disabled'*/); texture_floaterp->setDimentionsLimits(LLFloaterSimpleSnapshot::THUMBNAIL_SNAPSHOT_DIM_MAX, LLFloaterSimpleSnapshot::THUMBNAIL_SNAPSHOT_DIM_MIN); addDependentFloater(texture_floaterp); } floaterp->openFloater(); } floaterp->setFocus(TRUE); } void LLFloaterChangeItemThumbnail::onTexturePickerCommit(LLUUID id) { LLFloaterTexturePicker* floaterp = (LLFloaterTexturePicker*)mPickerHandle.get(); if (floaterp) { LLUUID asset_id = floaterp->getAssetID(); if (validateAsset(asset_id)) { setThumbnailId(asset_id); } else { LLNotificationsUtil::add("ThumbnailDimentionsLimit"); } } } void LLFloaterChangeItemThumbnail::setThumbnailId(const LLUUID &new_thumbnail_id) { LLInventoryObject* obj = getInventoryObject(); if (!obj) { return; } if (mTaskId.notNull()) { LL_ERRS() << "Not implemented yet" << LL_ENDL; return; } setThumbnailId(new_thumbnail_id, mItemId, obj); } void LLFloaterChangeItemThumbnail::setThumbnailId(const LLUUID& new_thumbnail_id, const LLUUID& object_id) { LLInventoryObject* obj = gInventory.getObject(object_id); if (!obj) { return; } setThumbnailId(new_thumbnail_id, object_id, obj); } void LLFloaterChangeItemThumbnail::setThumbnailId(const LLUUID& new_thumbnail_id, const LLUUID& object_id, LLInventoryObject* obj) { if (obj->getThumbnailUUID() != new_thumbnail_id) { LLSD updates; // At the moment server expects id as a string updates["thumbnail"] = LLSD().with("asset_id", new_thumbnail_id.asString()); LLViewerInventoryCategory* view_folder = dynamic_cast(obj); if (view_folder) { update_inventory_category(object_id, updates, NULL); } LLViewerInventoryItem* view_item = dynamic_cast(obj); if (view_item) { update_inventory_item(object_id, updates, NULL); } } } void LLFloaterChangeItemThumbnail::onButtonMouseEnter(LLUICtrl* button, const LLSD& param, EToolTipState state) { mTooltipState = state; std::string tooltip_text; std::string tooltip_name = "tooltip_" + button->getName(); if (hasString(tooltip_name)) { tooltip_text = getString(tooltip_name); } mToolTipTextBox->setValue(tooltip_text); } void LLFloaterChangeItemThumbnail::onButtonMouseLeave(LLUICtrl* button, const LLSD& param, EToolTipState state) { if (mTooltipState == state) { mTooltipState = TOOLTIP_NONE; LLSD tooltip_text; mToolTipTextBox->setValue(tooltip_text); } }