From 0bce5027e60a8b282d984c13364a7ad8de4979cd Mon Sep 17 00:00:00 2001 From: cinder <cinder@cinderblocks.biz> Date: Sat, 3 Dec 2022 13:40:14 -0600 Subject: [PATCH] Hey look, it's Day's hex editor... but without memleaks --- indra/newview/CMakeLists.txt | 4 + indra/newview/llfloaterhexeditor.cpp | 455 ++++++ indra/newview/llfloaterhexeditor.h | 60 + indra/newview/llhexeditor.cpp | 1252 +++++++++++++++++ indra/newview/llhexeditor.h | 163 +++ indra/newview/llinventorybridge.cpp | 30 + indra/newview/llinventorybridge.h | 2 + indra/newview/llviewerfloaterreg.cpp | 2 + indra/newview/llviewertexture.cpp | 47 + indra/newview/llviewertexture.h | 2 + .../default/xui/en/floater_hex_editor.xml | 14 + .../skins/default/xui/en/menu_inventory.xml | 15 + 12 files changed, 2046 insertions(+) create mode 100644 indra/newview/llfloaterhexeditor.cpp create mode 100644 indra/newview/llfloaterhexeditor.h create mode 100644 indra/newview/llhexeditor.cpp create mode 100644 indra/newview/llhexeditor.h create mode 100644 indra/newview/skins/default/xui/en/floater_hex_editor.xml diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 00844507f4c..9f8ff84c863 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -300,6 +300,7 @@ set(viewer_SOURCE_FILES llfloatergroups.cpp llfloaterhandler.cpp llfloaterhelpbrowser.cpp + llfloaterhexeditor.cpp llfloaterhoverheight.cpp llfloaterhowto.cpp llfloaterhud.cpp @@ -394,6 +395,7 @@ set(viewer_SOURCE_FILES llgroupoptions.cpp llgroupmgr.cpp llhasheduniqueid.cpp + llhexeditor.cpp llhints.cpp llhttpretrypolicy.cpp llhudeffect.cpp @@ -999,6 +1001,7 @@ set(viewer_HEADER_FILES llfloatergroups.h llfloaterhandler.h llfloaterhelpbrowser.h + llfloaterhexeditor.h llfloaterhoverheight.h llfloaterhowto.h llfloaterhud.h @@ -1095,6 +1098,7 @@ set(viewer_HEADER_FILES llgroupoptions.h llgroupmgr.h llhasheduniqueid.h + llhexeditor.h llhints.h llhttpretrypolicy.h llhudeffect.h diff --git a/indra/newview/llfloaterhexeditor.cpp b/indra/newview/llfloaterhexeditor.cpp new file mode 100644 index 00000000000..fe4cc3d249e --- /dev/null +++ b/indra/newview/llfloaterhexeditor.cpp @@ -0,0 +1,455 @@ +/** + * @file llfloaterhex.h + * @brief Hex Editor Floater made by Day + * @author Day Oh, Skills, Cinder + * + * $LicenseInfo:firstyear=2009&license=WTFPLV2$ + * + */ + +#include "llviewerprecompiledheaders.h" + +#include "llfloaterhexeditor.h" +#include "llbutton.h" +#include "llagent.h" +#include "lleconomy.h" +#include "llextendedstatus.h" +#include "llfloaterperms.h" +#include "llfloaterreg.h" +#include "llinventorymodel.h" +#include "llassetstorage.h" +#include "llfilesystem.h" +#include "llnotificationsutil.h" +#include "llviewermenufile.h" +#include "llviewerregion.h" +#include "llviewertexturelist.h" + + +LLFloaterHexEditor::LLFloaterHexEditor(const LLSD& key) +: LLFloater(key) +, mItem(nullptr) +, mAssetType(LLAssetType::AT_UNKNOWN) +, mEditor(nullptr) +{ } + +BOOL LLFloaterHexEditor::postBuild() +{ + mEditor = getChild<LLHexEditor>("hex"); + /* +#ifndef COLUMN_SPAN + // Set number of columns + U8 columns = U8(gSavedSettings.getU32("HexEditorColumns")); + editor->setColumns(columns); + // Reflect clamped U8ness in settings + gSavedSettings.setU32("HexEditorColumns", U32(columns)); +#endif + */ + handleSizing(); + + LLButton* upload_btn = getChild<LLButton>("upload_btn"); + upload_btn->setEnabled(false); + upload_btn->setLabelArg("[UPLOAD]", LLStringExplicit("Upload")); + upload_btn->setCommitCallback(boost::bind(&LLFloaterHexEditor::onClickUpload, this)); + + LLButton* save_btn = getChild<LLButton>("save_btn"); + save_btn->setEnabled(false); + save_btn->setCommitCallback(boost::bind(&LLFloaterHexEditor::onClickSave, this)); + + return TRUE; +} + +void LLFloaterHexEditor::onOpen(const LLSD& key) +{ + LLUUID inv_id = key.has("inv_id") ? key["inv_id"].asUUID() : LLUUID::null; + + LL_INFOS("HEX") << "Inventory ID: " << inv_id.asString() << LL_ENDL; + if (inv_id.isNull()) { return; } + + if (key.has("asset_type")) { mAssetType = static_cast<LLAssetType::EType>(key["asset_type"].asInteger()); } + + mItem = gInventory.getItem(inv_id); + + if (mItem) + { + std::string title = "Hex editor: " + mItem->getName(); + const char* asset_type_name = LLAssetType::lookup(mItem->getType()); + if(asset_type_name) + { + title.append(llformat(" (%s)", asset_type_name)); + } + setTitle(title); + } + + // Load the asset + mEditor->setVisible(FALSE); + childSetTextArg("status_text", "[STATUS]", LLStringExplicit("Loading...")); + download(mItem, imageCallback, assetCallback); + + refresh(); +} + +// static +void LLFloaterHexEditor::download(LLInventoryItem* item, loaded_callback_func onImage, LLGetAssetCallback onAsset) +{ + if (item == nullptr) + { + LL_WARNS("Hex") << "Could not download null pointer encountered!" << LL_ENDL; + return; + } + switch (item->getType()) + { + case LLAssetType::AT_TEXTURE: + { + LLPointer<LLViewerFetchedTexture> texture = LLViewerTextureManager::getFetchedTexture( + item->getAssetUUID(), FTT_DEFAULT, MIPMAP_YES, LLViewerTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE); + texture->setBoostLevel(LLViewerTexture::BOOST_PREVIEW); + texture->forceToSaveRawImage(0); + texture->setLoadedCallbackNoAux(onImage, 0, TRUE, FALSE, item, nullptr); + break; + } + case LLAssetType::AT_NOTECARD: + case LLAssetType::AT_SCRIPT: + case LLAssetType::AT_LSL_TEXT: // normal script download + case LLAssetType::AT_LSL_BYTECODE: + { + gAssetStorage->getInvItemAsset(LLHost(), + gAgent.getID(), + gAgent.getSessionID(), + item->getPermissions().getOwner(), + LLUUID::null, + item->getUUID(), + item->getAssetUUID(), + item->getType(), + onAsset, + item, // user_data + TRUE); + break; + } + case LLAssetType::AT_SOUND: + case LLAssetType::AT_CLOTHING: + case LLAssetType::AT_BODYPART: + case LLAssetType::AT_ANIMATION: + case LLAssetType::AT_GESTURE: + default: + { + gAssetStorage->getAssetData(item->getAssetUUID(), item->getType(), onAsset, item, TRUE); + break; + } + } +} + +// static +void LLFloaterHexEditor::imageCallback(BOOL success, + LLViewerFetchedTexture *src_vi, + LLImageRaw* src, + LLImageRaw* aux_src, + S32 discard_level, + BOOL final, + void* userdata) +{ + if (final) + { + const auto* item = static_cast<LLInventoryItem*>(userdata); + LLFloaterHexEditor* self = LLFloaterReg::findTypedInstance<LLFloaterHexEditor>("asset_hex_editor", + LLSD().with("inv_id", item->getUUID()).with("asset_type", item->getActualType())); + if (!self) return; + + if(!success) + { + self->childSetTextArg("status_text", "[STATUS]", LLStringExplicit("Unable to download asset.")); + return; + } + + U8* src_data = src->getData(); + S32 src_size = src->getDataSize(); + std::vector<U8> new_data; + for(S32 i = 0; i < src_size; ++i) + new_data.push_back(src_data[i]); + + self->mEditor->setValue(new_data); + self->mEditor->setVisible(TRUE); + self->childSetTextArg("status_text", "[STATUS]", LLStringExplicit("Note: Image data shown isn't the actual asset data, yet")); + + self->childSetEnabled("save_btn", false); + self->childSetEnabled("upload_btn", true); + self->childSetLabelArg("upload_btn", "[UPLOAD]", std::string("Upload (L$10)")); + } + else + { + src_vi->setBoostLevel(LLViewerTexture::BOOST_UI); + } +} + +// static +void LLFloaterHexEditor::assetCallback(const LLUUID& asset_uuid, + LLAssetType::EType type, + void* user_data, S32 status, LLExtStat ext_status) +{ + const auto item = static_cast<LLInventoryItem*>(user_data); + LLFloaterHexEditor* self = LLFloaterReg::findTypedInstance<LLFloaterHexEditor>("asset_hex_editor", + LLSD().with("inv_id", item->getUUID()).with("asset_type", item->getActualType())); + if (!self) return; + + if(status != 0 && item->getType() != LLAssetType::AT_NOTECARD) + { + self->childSetTextArg("status_text", "[STATUS]", LLStringExplicit("Unable to download asset.")); + return; + } + + LLFileSystem file(asset_uuid, type, LLFileSystem::READ); + S32 file_size = file.getSize(); + + auto buffer = std::make_unique<U8[]>(file_size); + if (buffer == nullptr) + { + LL_ERRS("Hex") << "Memory Allocation Failed" << LL_ENDL; + return; + } + + if (!file.open() || !file.read(buffer.get(), file_size)) + { + LL_WARNS("Hex") << "Could not read " << asset_uuid.asString() << " into memory" << LL_ENDL; + } + file.close(); + + std::vector<U8> new_data; + for(S32 i = 0; i < file_size; ++i) + new_data.push_back(buffer[i]); + + self->mEditor->setValue(new_data); + self->mEditor->setVisible(TRUE); + self->childSetTextArg("status_text", "[STATUS]", LLStringUtil::null); + + self->childSetEnabled("upload_btn", true); + self->childSetEnabled("save_btn", false); + if(item->getPermissions().allowModifyBy(gAgent.getID())) + { + switch(item->getType()) + { + case LLAssetType::AT_TEXTURE: + case LLAssetType::AT_ANIMATION: + case LLAssetType::AT_SOUND: + self->childSetLabelArg("upload_btn", "[UPLOAD]", LLStringExplicit("Upload (L$10)")); + break; + case LLAssetType::AT_LANDMARK: + case LLAssetType::AT_CALLINGCARD: + self->childSetEnabled("upload_btn", false); + self->childSetEnabled("save_btn", false); + break; + default: + self->childSetEnabled("save_btn", true); + break; + } + } + else + { + switch(item->getType()) + { + case LLAssetType::AT_TEXTURE: + case LLAssetType::AT_ANIMATION: + case LLAssetType::AT_SOUND: + self->childSetLabelArg("upload_btn", "[UPLOAD]", LLStringExplicit("Upload (L$10)")); + break; + default: + break; + } + } + + // Never enable save if it's a local inventory item + if(gInventory.isObjectDescendentOf(item->getUUID(), gLocalInventory)) + { + self->childSetEnabled("save_btn", false); + } +} + +void LLFloaterHexEditor::onClickUpload() +{ + const LLInventoryItem* item = mItem; + + LLTransactionID transaction_id; + transaction_id.generate(); + LLUUID fake_asset_id = transaction_id.makeAssetID(gAgent.getSecureSessionID()); + + std::vector<U8> value = mEditor->getValue(); + size_t val_size = value.size(); + auto buffer = std::make_unique<U8[]>(val_size); + for(size_t i = 0; i < val_size; ++i) + buffer[i] = value[i]; + value.clear(); + + LLFileSystem file(fake_asset_id, item->getType(), LLFileSystem::APPEND); + if (!file.open() || !file.write(buffer.get(), val_size)) + { + LLSD args = LLSD().with("MESSAGE", "Couldn't write data to file"); + LLNotificationsUtil::add("GenericAlert", args); + return; + } + file.close(); + + LLAssetStorage::LLStoreAssetCallback callback = nullptr; + S32 expected_upload_cost = LLGlobalEconomy::getInstance()->getPriceUpload(); + + if(item->getType() == LLAssetType::AT_GESTURE || + item->getType() == LLAssetType::AT_LSL_TEXT || + item->getType() == LLAssetType::AT_NOTECARD) + // gestures, notecards and scripts, create an item first + { + create_inventory_item(gAgent.getID(), + gAgent.getSessionID(), + item->getParentUUID(), + LLTransactionID::tnull, + item->getName(), + fake_asset_id.asString(), + item->getType(), + item->getInventoryType(), + item->getFlags(), + PERM_ITEM_UNRESTRICTED, + LLPointer<LLInventoryCallback>(nullptr)); + } + else + { + LLResourceUploadInfo::ptr_t uploadInfo( + new LLResourceUploadInfo(transaction_id, + item->getType(), + item->getName(), + item->getDescription(), + 0, LLFolderType::FT_NONE, + item->getInventoryType(), + LLFloaterPerms::getNextOwnerPerms("Uploads"), + LLFloaterPerms::getGroupPerms("Uploads"), + LLFloaterPerms::getEveryonePerms("Uploads"), + expected_upload_cost)); + upload_new_resource(uploadInfo); + } +} + +void LLFloaterHexEditor::onSavedAsset(const LLUUID& id, const LLSD& response) +{ + // TODO: Something +} + +void LLFloaterHexEditor::onClickSave() +{ + LLInventoryItem* item = mItem; + + LLTransactionID transaction_id; + transaction_id.generate(); + const LLUUID fake_asset_id = transaction_id.makeAssetID(gAgent.getSecureSessionID()); + + std::vector<U8> value = mEditor->getValue(); + size_t val_size = value.size(); + auto buffer = std::make_unique<U8[]>(val_size); + for(size_t i = 0; i < val_size; ++i) + buffer[i] = value[i]; + value.clear(); + + LLFileSystem file(fake_asset_id, item->getType(), LLFileSystem::APPEND); + if (file.open() && file.getMaxSize() > val_size) + { + if (!file.write(buffer.get(), val_size)) + { + LLSD args = LLSD().with("MESSAGE", "Could not write data to file"); + LLNotificationsUtil::add("GenericAlert", args); + return; + } + file.close(); + } + + + std::string url; + LLResourceUploadInfo::ptr_t uploadInfo; + + switch(item->getType()) + { + case LLAssetType::AT_LSL_TEXT: + { + url = gAgent.getRegion()->getCapability("UpdateScriptAgent"); + uploadInfo = std::make_shared<LLScriptAssetUpload>(mItem->getUUID(), + std::string(reinterpret_cast<char*>(buffer.get())), + boost::bind(&LLFloaterHexEditor::onSavedAsset, this, _1, _4)); + break; + } + case LLAssetType::AT_GESTURE: + { + url = gAgent.getRegion()->getCapability("UpdateGestureAgentInventory"); + uploadInfo = std::make_shared<LLBufferedAssetUploadInfo>(mItem->getUUID(), LLAssetType::AT_GESTURE, + std::string(reinterpret_cast<char*>(buffer.get())), + boost::bind(&LLFloaterHexEditor::onSavedAsset, this, _1, _4)); + break; + } + case LLAssetType::AT_NOTECARD: + { + url = gAgent.getRegion()->getCapability("UpdateNotecardAgentInventory"); + uploadInfo = std::make_shared<LLBufferedAssetUploadInfo>(mItem->getUUID(), LLAssetType::AT_NOTECARD, + std::string(reinterpret_cast<char*>(buffer.get())), + boost::bind(&LLFloaterHexEditor::onSavedAsset, this, _1, _4)); + break; + } + case LLAssetType::AT_SETTINGS: + { + url = gAgent.getRegion()->getCapability("UpdateSettingsAgentInventory"); + uploadInfo = std::make_shared<LLBufferedAssetUploadInfo>(mItem->getUUID(), LLAssetType::AT_SETTINGS, + std::string(reinterpret_cast<char*>(buffer.get())), + boost::bind(&LLFloaterHexEditor::onSavedAsset, this, _1, _4)); + break; + } + default: + { + childSetTextArg("status_text", "[STATUS]", LLStringExplicit("Saving...")); + gAssetStorage->storeAssetData(transaction_id, item->getType(), onSaveComplete, item); + break; + } + } + + if(!url.empty() && uploadInfo.get()) + { + LLViewerAssetUpload::EnqueueInventoryUpload(url, uploadInfo); + } +} + +void LLFloaterHexEditor::onSaveComplete(const LLUUID& asset_uuid, void* user_data, S32 status, LLExtStat ext_status) +{ + const auto item = static_cast<LLInventoryItem*>(user_data); + LLFloaterHexEditor* self = LLFloaterReg::findTypedInstance<LLFloaterHexEditor>("asset_hex_editor", + LLSD().with("inv_id", item->getUUID()).with("asset_type", item->getActualType())); + + self->childSetTextArg("status_text", "[STATUS]", LLStringUtil::null); + + if(item && (status == 0)) + { + LLPointer<LLViewerInventoryItem> new_item = new LLViewerInventoryItem(item); + new_item->setDescription(item->getDescription()); + //new_item->setTransactionID(info->mTransactionID); + new_item->setAssetUUID(asset_uuid); + new_item->updateServer(FALSE); + gInventory.updateItem(new_item); + gInventory.notifyObservers(); + } + else + { + LLSD args = LLSD().with("MESSAGE", llformat("Save failed with status %d, also %d", status, ext_status)); + LLNotificationsUtil::add("GenericAlert", args); + } +} + +void LLFloaterHexEditor::onCommitColumnCount(LLUICtrl *control) +{ + if (control) + { + U8 columns = llclamp<U8>((U8)llfloor(control->getValue().asReal()), 0x00, 0xFF); + mEditor->setColumns(columns); + handleSizing(); + } +} + +void LLFloaterHexEditor::handleSizing() +{ + // Reshape a little based on columns + S32 min_width = static_cast<S32>(mEditor->getSuggestedWidth(MIN_COLS)) + 20; + setResizeLimits(min_width, getMinHeight()); + if(getRect().getWidth() < min_width) + { + reshape(min_width, getRect().getHeight(), FALSE); + mEditor->reshape(mEditor->getRect().getWidth(), mEditor->getRect().getHeight(), TRUE); + } +} diff --git a/indra/newview/llfloaterhexeditor.h b/indra/newview/llfloaterhexeditor.h new file mode 100644 index 00000000000..1b70eba4744 --- /dev/null +++ b/indra/newview/llfloaterhexeditor.h @@ -0,0 +1,60 @@ +/** + * @file llfloaterhex.h + * @brief Hex Editor Floater made by Day + * @author Day Oh, Skills, Cinder + * + * $LicenseInfo:firstyear=2009&license=WTFPLV2$ + * + */ + +#ifndef LL_FLOATERHEX_H +#define LL_FLOATERHEX_H + +#include "llassetstorage.h" +#include "llassettype.h" +#include "llextendedstatus.h" +#include "llfloater.h" +#include "llhexeditor.h" +#include "llinventory.h" +#include "llviewertexture.h" + +class LLFloaterHexEditor : public LLFloater +{ +public: + LLFloaterHexEditor(const LLSD& key); + + void onOpen(const LLSD& key) override; + BOOL postBuild(); + + LLInventoryItem* mItem; + LLAssetType::EType mAssetType; + LLHexEditor* mEditor; + + static void imageCallback(BOOL success, + LLViewerFetchedTexture *src_vi, + LLImageRaw* src, + LLImageRaw* aux_src, + S32 discard_level, + BOOL final, + void* userdata); + static void assetCallback(const LLUUID& asset_uuid, + LLAssetType::EType type, + void* user_data, S32 status, LLExtStat ext_status); + + + + static void download(LLInventoryItem* item, loaded_callback_func onImage, LLGetAssetCallback onAsset); + static void onSaveComplete(const LLUUID& asset_uuid, void* user_data, S32 status, LLExtStat ext_status); + +private: + ~LLFloaterHexEditor() override = default; + + void onClickSave(); + void onClickUpload(); + + void onSavedAsset(const LLUUID& id, const LLSD& response); + void onCommitColumnCount(LLUICtrl *control); + void handleSizing(); +}; + +#endif diff --git a/indra/newview/llhexeditor.cpp b/indra/newview/llhexeditor.cpp new file mode 100644 index 00000000000..ec8b3d50eeb --- /dev/null +++ b/indra/newview/llhexeditor.cpp @@ -0,0 +1,1252 @@ +/** + * @file dohexeditor.cpp + * @brief DOHexEditor Widget + * @author Day Oh, Skills, Cinder + * + * $LicenseInfo:firstyear=2009&license=WTFPLV2$ + * + */ + +#include "llviewerprecompiledheaders.h" + +#include "linden_common.h" + +#include "llhexeditor.h" +#include "llfocusmgr.h" +#include "llscrollcontainer.h" +#include "llkeyboard.h" +#include "llclipboard.h" +#include "llwindow.h" // setCursor + +#include "llview.h" +#include "lllocalcliprect.h" + + +static LLDefaultChildRegistry::Register<LLHexEditor> r1("hex_editor"); + +static constexpr size_t SCROLLBAR_SIZE = 16; +static constexpr S32 VERTICAL_MULTIPLE = 16; + +LLHexEditor::LLHexEditor(const Params & p) +: LLUICtrl(p) +, LLEditMenuHandler() +, mName(p.name) +, mColumns(16) +, mCursorPos(0) +, mSecondNibble(false) +, mSelecting(false) +, mHasSelection(false) +, mInData(false) +, mSelectionStart(0) +, mSelectionEnd(0) +{ + mGLFont = LLFontGL::getFontMonospace(); + + mTextRect.setOriginAndSize( + 5, // border + padding + 1, // border + getRect().getWidth() - SCROLLBAR_SIZE - 6, + getRect().getHeight() - 5); + + S32 line_height = mGLFont->getLineHeight(); + S32 page_size = mTextRect.getHeight() / line_height; + S32 lines_in_doc = getLineCount(); + + LLRect scroll_rect; + scroll_rect.setOriginAndSize( + getRect().getWidth() - SCROLLBAR_SIZE, + 1, + SCROLLBAR_SIZE, + getRect().getHeight() - 1); + + LLScrollbar::Params sbparams; + sbparams.name("Scrollbar"); + sbparams.rect(scroll_rect); + sbparams.orientation(LLScrollbar::VERTICAL); + sbparams.doc_size(lines_in_doc); //mInnerRect.getHeight() + sbparams.doc_pos(0); + sbparams.page_size(page_size); //mInnerRect.getHeight() + sbparams.step_size(VERTICAL_MULTIPLE); + sbparams.follows.flags(FOLLOWS_RIGHT | FOLLOWS_TOP | FOLLOWS_BOTTOM); + + mScrollbar = LLUICtrlFactory::create<LLScrollbar> (sbparams); + LLView::addChild( mScrollbar ); + mScrollbar->setVisible( true ); + mScrollbar->setEnabled( true ); + mScrollbar->setFollows(FOLLOWS_RIGHT | FOLLOWS_TOP | FOLLOWS_BOTTOM); + //mScrollbar->setOnScrollEndCallback(NULL, NULL); + + LLRect border_rect = LLRect(0, getRect().getHeight(), getRect().getWidth(), 0); //getLocalRect(); +// border_rect.mBottom += BTN_HEIGHT_SMALL; + LLViewBorder::Params vbparams; + vbparams.name("text ed border"); + vbparams.rect(border_rect); + mBorder = LLUICtrlFactory::create<LLViewBorder> (vbparams); + addChild(mBorder); + + changedLength(); + mUndoBuffer = new LLUndoBuffer(LLUndoHex::create, 128); +} + +LLHexEditor::~LLHexEditor() +{ + delete mUndoBuffer; + gFocusMgr.releaseFocusIfNeeded(this); + if(LLEditMenuHandler::gEditMenuHandler == this) + { + LLEditMenuHandler::gEditMenuHandler = NULL; + } +} + +BOOL LLHexEditor::postBuild() +{ + return TRUE; +} + +void LLHexEditor::setValue(const LLSD& value) +{ + mValue = value.asBinary(); + changedLength(); +} + +LLSD LLHexEditor::getValue() const +{ + return LLSD(mValue); +} + +void LLHexEditor::setColumns(U8 columns) +{ + mColumns = llclamp<U8>(llfloor(columns), 8, 64); + changedLength(); +} + +U32 LLHexEditor::getLineCount() const +{ + U32 lines = mValue.size(); + lines /= mColumns; + lines++; // incomplete or extra line at bottom + return lines; +} + +void LLHexEditor::getPosAndContext(S32 x, S32 y, BOOL force_context, U32& pos, BOOL& in_data, BOOL& second_nibble) const +{ + pos = 0; + + F32 line_height = mGLFont->getLineHeight(); + F32 char_width = mGLFont->getWidthF32("."); + F32 data_column_width = char_width * 3; // " 00"; + F32 text_x = mTextRect.mLeft; + F32 text_x_data = text_x + (char_width * 10.1f); // "00000000 ", dunno why it's a fraction off + F32 text_x_ascii = text_x_data + (data_column_width * mColumns) + (char_width * 2); + F32 text_y = (F32)(mTextRect.mTop - line_height); + U32 first_line = mScrollbar->getDocPos(); + //U32 last_line = first_line + mScrollbar->getPageSize(); // don't -2 from scrollbar sizes + S32 first_line_y = text_y - line_height; + + S32 ly = -(y - first_line_y); // negative vector from first line to y + ly -= 5; // slight skew + S32 line = ly / line_height; + if(line < 0) line = 0; + line += first_line; + + if (!force_context) + { + in_data = x < (text_x_ascii - char_width); // char width for less annoying + } + S32 lx = x; + S32 offset; + if(in_data) + { + lx -= char_width; // subtracting char width because idk + lx -= text_x_data; + offset = lx / data_column_width; + + // Now, which character + S32 rem = static_cast<S32>(static_cast<F32>(lx) - (data_column_width * offset) - (char_width * 0.25)); + if(rem > 0) + { + if(rem > char_width) + { + offset++; // next byte + second_nibble = FALSE; + } + else second_nibble = TRUE; + } + else second_nibble = FALSE; + } + else + { + second_nibble = FALSE; + lx += char_width; // adding char width because idk + lx -= text_x_ascii; + offset = lx / char_width; + } + if(offset < 0) offset = 0; + if(offset >= mColumns)//offset = mColumns - 1; + { + offset = 0; + line++; + second_nibble = FALSE; + } + + pos = (line * mColumns) + offset; + if(pos > mValue.size()) pos = mValue.size(); + if(pos == mValue.size()) + { + second_nibble = FALSE; + } +} + +void LLHexEditor::changedLength() +{ + S32 line_height = mGLFont->getLineHeight(); + LL_INFOS() << "line_height: " << line_height << LL_ENDL; + + S32 page_size = mTextRect.getHeight() / line_height; + page_size -= 2; // don't count the spacer and header + LL_INFOS() << "page_size: " << page_size << LL_ENDL; + LL_INFOS() << " getLineCount: " << getLineCount() << LL_ENDL; + + mScrollbar->setDocSize(getLineCount()); + mScrollbar->setPageSize(page_size); + + moveCursor(mCursorPos, mSecondNibble); // cursor was off end after undo of paste +} + +void LLHexEditor::reshape(S32 width, S32 height, BOOL called_from_parent) +{ + LLView::reshape( width, height, called_from_parent ); + mTextRect.setOriginAndSize( + 5, // border + padding + 1, // border + getRect().getWidth() - SCROLLBAR_SIZE - 6, + getRect().getHeight() - 5); + LLRect scrollrect; + scrollrect.setOriginAndSize( + getRect().getWidth() - SCROLLBAR_SIZE, + 1, + SCROLLBAR_SIZE, + getRect().getHeight() - 1); + mScrollbar->setRect(scrollrect); + mBorder->setRect(LLRect(0, getRect().getHeight(), getRect().getWidth(), 0)); + changedLength(); +} + +void LLHexEditor::setFocus(BOOL b) +{ + if (b) + { + LLEditMenuHandler::gEditMenuHandler = this; + } + else + { + mSelecting = FALSE; + gFocusMgr.releaseFocusIfNeeded(this); + if(LLEditMenuHandler::gEditMenuHandler == this) + { + LLEditMenuHandler::gEditMenuHandler = NULL; + } + } + LLUICtrl::setFocus(b); +} + +F32 LLHexEditor::getSuggestedWidth(U8 cols) +{ + cols = cols>1?cols:mColumns; + F32 char_width = mGLFont->getWidthF32("."); + F32 data_column_width = char_width * 3; // " 00"; + F32 text_x = mTextRect.mLeft; + F32 text_x_data = text_x + (char_width * 10.1f); // "00000000 ", dunno why it's a fraction off + F32 text_x_ascii = text_x_data + (data_column_width * cols) + (char_width * 2); + F32 suggested_width = text_x_ascii + (char_width * cols); + suggested_width += mScrollbar->getRect().getWidth(); + suggested_width += 10.0f; + return suggested_width; +} + +U32 LLHexEditor::getProperSelectionStart() const +{ + return (mSelectionStart < mSelectionEnd) ? mSelectionStart : mSelectionEnd; +} + +U32 LLHexEditor::getProperSelectionEnd() const +{ + return (mSelectionStart < mSelectionEnd) ? mSelectionEnd : mSelectionStart; +} + +BOOL LLHexEditor::handleScrollWheel(S32 x, S32 y, S32 clicks) const +{ + return mScrollbar->handleScrollWheel( 0, 0, clicks ); +} + +BOOL LLHexEditor::handleMouseDown(S32 x, S32 y, MASK mask) +{ + BOOL handled = FALSE; + handled = LLView::childrenHandleMouseDown(x, y, mask) != NULL; + if(!handled) + { + setFocus(TRUE); + gFocusMgr.setMouseCapture(this); + handled = TRUE; + if(!mSelecting) + { + if(mask & MASK_SHIFT) + { + // extend a selection + getPosAndContext(x, y, FALSE, mCursorPos, mInData, mSecondNibble); + mSelectionEnd = mCursorPos; + mHasSelection = (mSelectionStart != mSelectionEnd); + mSelecting = TRUE; + } + else + { + // start selecting + getPosAndContext(x, y, FALSE, mCursorPos, mInData, mSecondNibble); + mSelectionStart = mCursorPos; + mSelectionEnd = mCursorPos; + mHasSelection = FALSE; + mSelecting = TRUE; + } + } + } + return handled; +} + +BOOL LLHexEditor::handleHover(S32 x, S32 y, MASK mask) +{ + BOOL handled = FALSE; + if(!hasMouseCapture()) + { + handled = childrenHandleHover(x, y, mask) != NULL; + } + if(!handled && mSelecting && hasMouseCapture()) + { + // continuation of selecting + getPosAndContext(x, y, TRUE, mCursorPos, mInData, mSecondNibble); + mSelectionEnd = mCursorPos; + mHasSelection = (mSelectionStart != mSelectionEnd); + handled = TRUE; + } + return handled; +} + +BOOL LLHexEditor::handleMouseUp(S32 x, S32 y, MASK mask) +{ + BOOL handled = FALSE; + handled = LLView::childrenHandleMouseUp(x, y, mask) != NULL; + if(!handled && mSelecting && hasMouseCapture()) + { + gFocusMgr.setMouseCapture(NULL); + mSelecting = FALSE; + } + return handled; +} + +BOOL LLHexEditor::handleKeyHere(KEY key, MASK mask) +{ + return FALSE; +} + +BOOL LLHexEditor::handleKey(KEY key, MASK mask, BOOL called_from_parent) +{ + BOOL handled = FALSE; + + BOOL moved_cursor = FALSE; + U32 old_cursor = mCursorPos; + U32 cursor_line = mCursorPos / mColumns; + U32 doc_first_line = 0; + U32 doc_last_line = mValue.size() / mColumns; + //U32 first_line = mScrollbar->getDocPos(); + //U32 last_line = first_line + mScrollbar->getPageSize(); // don't -2 from scrollbar sizes + U32 beginning_of_line = mCursorPos - (mCursorPos % mColumns); + U32 end_of_line = beginning_of_line + mColumns - 1; + + handled = TRUE; + switch( key ) + { + + // Movement keys + + case KEY_UP: + if(cursor_line > doc_first_line) + { + moveCursor(mCursorPos - mColumns, mSecondNibble); + moved_cursor = TRUE; + } + break; + case KEY_DOWN: + if(cursor_line < doc_last_line) + { + moveCursor(mCursorPos + mColumns, mSecondNibble); + moved_cursor = TRUE; + } + break; + case KEY_LEFT: + if(mCursorPos) + { + if(!mSecondNibble) moveCursor(mCursorPos - 1, FALSE); + else moveCursor(mCursorPos, FALSE); + moved_cursor = TRUE; + } + break; + case KEY_RIGHT: + moveCursor(mCursorPos + 1, FALSE); + moved_cursor = TRUE; + break; + case KEY_PAGE_UP: + mScrollbar->pageUp(1); + break; + case KEY_PAGE_DOWN: + mScrollbar->pageDown(1); + break; + case KEY_HOME: + if(mask & MASK_CONTROL) + moveCursor(0, FALSE); + else + moveCursor(beginning_of_line, FALSE); + moved_cursor = TRUE; + break; + case KEY_END: + if(mask & MASK_CONTROL) + moveCursor(mValue.size(), FALSE); + else + moveCursor(end_of_line, FALSE); + moved_cursor = TRUE; + break; + + // Special + + case KEY_INSERT: + gKeyboard->toggleInsertMode(); + break; + + case KEY_ESCAPE: + gFocusMgr.releaseFocusIfNeeded(this); + break; + + // Editing + case KEY_BACKSPACE: + if(mHasSelection) + { + U32 start = getProperSelectionStart(); + U32 end = getProperSelectionEnd(); + del(start, end - 1, TRUE); + moveCursor(start, FALSE); + } + else if(mCursorPos && (!mSecondNibble)) + { + del(mCursorPos - 1, mCursorPos - 1, TRUE); + moveCursor(mCursorPos - 1, FALSE); + } + break; + + case KEY_DELETE: + if(mHasSelection) + { + U32 start = getProperSelectionStart(); + U32 end = getProperSelectionEnd(); + del(start, end - 1, TRUE); + moveCursor(start, FALSE); + } + else if((mCursorPos != mValue.size()) && (!mSecondNibble)) + { + del(mCursorPos, mCursorPos, TRUE); + } + break; + + default: + handled = FALSE; + break; + } + + if(moved_cursor) + { + // Selecting and deselecting + if(mask & MASK_SHIFT) + { + if(!mHasSelection) mSelectionStart = old_cursor; + mSelectionEnd = mCursorPos; + } + else + { + mSelectionStart = mCursorPos; + mSelectionEnd = mCursorPos; + } + mHasSelection = mSelectionStart != mSelectionEnd; + } + + return handled; +} + +BOOL LLHexEditor::handleUnicodeChar(llwchar uni_char, BOOL called_from_parent) +{ + U8 c = uni_char & 0xff; + if(mInData) + { + if(c > 0x39) + { + if(c > 0x46) c -= 0x20; + if(c >= 0x41 && c <= 0x46) c = (c & 0x0f) + 0x09; + else return TRUE; + } + else if(c < 0x30) return TRUE; + else c &= 0x0f; + } + + if(uni_char < 0x20) return FALSE; + + if( (LL_KIM_INSERT == gKeyboard->getInsertMode() && (!mHasSelection)) + || (!mHasSelection && (mCursorPos == mValue.size())) )// last byte? always insert + { + // Todo: this should overwrite if there's a selection + if(!mInData) + { + std::vector<U8> new_data; + new_data.push_back(c); + insert(mCursorPos, new_data, TRUE); + moveCursor(mCursorPos + 1, FALSE); + } + else if(!mSecondNibble) + { + c <<= 4; + std::vector<U8> new_data; + new_data.push_back(c); + insert(mCursorPos, new_data, TRUE); + moveCursor(mCursorPos, TRUE); + } + else + { + c |= (mValue[mCursorPos] & 0xF0); + std::vector<U8> new_data; + new_data.push_back(c); + overwrite(mCursorPos, mCursorPos, new_data, TRUE); + moveCursor(mCursorPos + 1, FALSE); + } + } + else // overwrite mode + { + if(mHasSelection) + { + if(mInData) c <<= 4; + std::vector<U8> new_data; + new_data.push_back(c); + U8 start = getProperSelectionStart(); + overwrite(start, getProperSelectionEnd() - 1, new_data, TRUE); + if(mInData) moveCursor(start, TRUE); // we only entered a nibble + else moveCursor(start + 1, FALSE); // we only entered a byte + } + else if(!mInData) + { + std::vector<U8> new_data; + new_data.push_back(c); + overwrite(mCursorPos, mCursorPos, new_data, TRUE); + moveCursor(mCursorPos + 1, FALSE); + } + else if(!mSecondNibble) + { + c <<= 4; + c |= (mValue[mCursorPos] & 0x0F); + std::vector<U8> new_data; + new_data.push_back(c); + overwrite(mCursorPos, mCursorPos, new_data, TRUE); + moveCursor(mCursorPos, TRUE); + } + else + { + c |= (mValue[mCursorPos] & 0xF0); + std::vector<U8> new_data; + new_data.push_back(c); + overwrite(mCursorPos, mCursorPos, new_data, TRUE); + moveCursor(mCursorPos + 1, FALSE); + } + } + + return TRUE; +} + +BOOL LLHexEditor::handleUnicodeCharHere(llwchar uni_char) +{ + return FALSE; +} + +void LLHexEditor::draw() +{ + S32 left = 0; + S32 top = getRect().getHeight(); + S32 right = getRect().getWidth(); + S32 bottom = 0; + + BOOL has_focus = gFocusMgr.getKeyboardFocus() == reinterpret_cast<LLFocusableElement*>(this); + + F32 line_height = mGLFont->getLineHeight(); + F32 char_width = mGLFont->getWidthF32("."); + F32 data_column_width = char_width * 3; // " 00"; + F32 text_x = mTextRect.mLeft; + F32 text_x_data = text_x + (char_width * 10.1f); // "00000000 ", dunno why it's a fraction off +#ifdef COLUMN_SPAN + mColumns = (right - char_width * 2 - text_x_data - mScrollbar->getRect().getWidth()) / (char_width * 4); // touch this if you dare... +#endif + F32 text_x_ascii = text_x_data + (data_column_width * mColumns) + (char_width * 2); + F32 text_y = (F32)(mTextRect.mTop - line_height); + + U32 data_length = mValue.size(); + U32 line_count = getLineCount(); + U32 first_line = mScrollbar->getDocPos(); + U32 last_line = first_line + mScrollbar->getPageSize(); // don't -2 from scrollbar sizes + + LLRect clip(getRect()); + clip.mRight = mScrollbar->getRect().mRight; + clip.mLeft -= 10; + clip.mBottom -= 10; + LLLocalClipRect bgclip(clip); + + // Background + gl_rect_2d(left, top, right - SCROLLBAR_SIZE, bottom, LLColor4::white); + + // Let's try drawing some helpful guides + LLColor4 guide_color_light = LLColor4(0.95f, 0.95f, 0.95f); + LLColor4 guide_color_dark = LLColor4(0.9f, 0.9f, 0.9f); + for(U32 col = 0; col < mColumns; col += 2) + { + // Behind hex + F32 box_left = text_x_data + (col * data_column_width) + 2; // skew 2 + F32 box_right = box_left + data_column_width; + gl_rect_2d(box_left, top, box_right, bottom, (col & 3) ? guide_color_light : guide_color_dark); + // Behind ASCII + //box_left = text_x_ascii + (col * char_width) - 1; // skew 1 + //box_right = box_left + char_width; + //gl_rect_2d(box_left, top, box_right, bottom, guide_color); + } + + + // Scrollbar & border (drawn twice?) + mBorder->setKeyboardFocusHighlight(has_focus); + LLView::draw(); + + + LLLocalClipRect textrect_clip(mTextRect); + + + // Selection stuff is reused + U32 selection_start = getProperSelectionStart(); + U32 selection_end = getProperSelectionEnd(); + U32 selection_first_line = selection_start / mColumns; + U32 selection_last_line = selection_end / mColumns; + U32 selection_start_column = selection_start % mColumns; + U32 selection_end_column = selection_end % mColumns; + + // Don't pretend a selection there is visible + if(!selection_end_column) + { + selection_last_line--; + selection_end_column = mColumns; + } + + if(mHasSelection) + { + LLColor4 selection_color_context(LLColor4::black); + LLColor4 selection_color_not_context(LLColor4::grey3); + LLColor4 selection_color_data(selection_color_not_context); + LLColor4 selection_color_ascii(selection_color_not_context); + if(mInData) selection_color_data = selection_color_context; + else selection_color_ascii = selection_color_context; + + + // Setup for selection in data + F32 selection_pixel_x_base = text_x_data + char_width - 3; // skew 3 + F32 selection_pixel_x_right_base = selection_pixel_x_base + (data_column_width * mColumns) - char_width + 4; + F32 selection_pixel_x; + F32 selection_pixel_x_right; + F32 selection_pixel_y = (F32)(mTextRect.mTop - line_height) - 3; // skew 3; + selection_pixel_y -= line_height * 2; + selection_pixel_y -= line_height * (S32(selection_first_line) - S32(first_line)); + + // Selection in data, First line + if(selection_first_line >= first_line && selection_first_line <= last_line) + { + selection_pixel_x = selection_pixel_x_base; + selection_pixel_x += (data_column_width * selection_start_column); + if(selection_first_line == selection_last_line) + { + // Select to last character + selection_pixel_x_right = selection_pixel_x_base + (data_column_width * selection_end_column); + selection_pixel_x_right -= (char_width - 4); + } + else + { + // Select to end of line + selection_pixel_x_right = selection_pixel_x_right_base; + } + gl_rect_2d(selection_pixel_x, selection_pixel_y + line_height, selection_pixel_x_right, selection_pixel_y, selection_color_data); + } + + // Selection in data, Middle lines + for(U32 line = selection_first_line + 1; line < selection_last_line; line++) + { + selection_pixel_y -= line_height; + if(line >= first_line && line <= last_line) + { + gl_rect_2d(selection_pixel_x_base, selection_pixel_y + line_height, selection_pixel_x_right_base, selection_pixel_y, selection_color_data); + } + } + + // Selection in data, Last line + if(selection_first_line != selection_last_line + && selection_last_line >= first_line && selection_last_line <= last_line) + { + selection_pixel_x_right = selection_pixel_x_base + (data_column_width * selection_end_column); + selection_pixel_x_right -= (char_width - 4); + selection_pixel_y -= line_height; + gl_rect_2d(selection_pixel_x_base, selection_pixel_y + line_height, selection_pixel_x_right, selection_pixel_y, selection_color_data); + } + + selection_pixel_y = (F32)(mTextRect.mTop - line_height) - 3; // skew 3; + selection_pixel_y -= line_height * 2; + selection_pixel_y -= line_height * (S32(selection_first_line) - S32(first_line)); + + // Setup for selection in ASCII + selection_pixel_x_base = text_x_ascii - 1; + selection_pixel_x_right_base = selection_pixel_x_base + (char_width * mColumns); + + // Selection in ASCII, First line + if(selection_first_line >= first_line && selection_first_line <= last_line) + { + selection_pixel_x = selection_pixel_x_base; + selection_pixel_x += (char_width * selection_start_column); + if(selection_first_line == selection_last_line) + { + // Select to last character + selection_pixel_x_right = selection_pixel_x_base + (char_width * selection_end_column); + } + else + { + // Select to end of line + selection_pixel_x_right = selection_pixel_x_right_base; + } + gl_rect_2d(selection_pixel_x, selection_pixel_y + line_height, selection_pixel_x_right, selection_pixel_y, selection_color_ascii); + } + + // Selection in ASCII, Middle lines + for(U32 line = selection_first_line + 1; line < selection_last_line; line++) + { + selection_pixel_y -= line_height; + if(line >= first_line && line <= last_line) + { + gl_rect_2d(selection_pixel_x_base, selection_pixel_y + line_height, selection_pixel_x_right_base, selection_pixel_y, selection_color_ascii); + } + } + + // Selection in ASCII, Last line + if(selection_first_line != selection_last_line + && selection_last_line >= first_line && selection_last_line <= last_line) + { + selection_pixel_x_right = selection_pixel_x_base + (char_width * selection_end_column); + selection_pixel_y -= line_height; + gl_rect_2d(selection_pixel_x_base, selection_pixel_y + line_height, selection_pixel_x_right, selection_pixel_y, selection_color_ascii); + } + } + + + // Insert/Overwrite + std::string text = (LL_KIM_OVERWRITE == gKeyboard->getInsertMode()) ? "OVERWRITE" : "INSERT"; + mGLFont->renderUTF8(text, 0, text_x, text_y, LLColor4::purple); + // Offset on top + text = ""; + for(U32 i = 0; i < mColumns; i++) + { + text.append(llformat(" %02X", i)); + } + mGLFont->renderUTF8(text, 0, text_x_data, text_y, LLColor4::blue); + // Size + { + size_t size = mValue.size(); + std::string size_desc; + if(size < 1000) size_desc = llformat("%d bytes", size); + else + { + if(size < 1000000) + { + size_desc = llformat("%f", F32(size) / 1000.0f); + size_t i = size_desc.length() - 1; + for(; i && size_desc.substr(i, 1) == "0"; i--); + if(size_desc.substr(i, 1) == ".") i--; + size_desc = size_desc.substr(0, i + 1); + size_desc.append(" KB"); + } + else + { + size_desc = llformat("%f", F32(size) / 1000000.0f); + size_t i = size_desc.length() - 1; + for(; i && size_desc.substr(i, 1) == "0"; i--); + if(size_desc.substr(i, 1) == ".") i--; + size_desc = size_desc.substr(0, i + 1); + size_desc.append(" MB"); + } + } + F32 x = text_x_ascii; + x += (char_width * (mColumns - size_desc.length())); + mGLFont->renderUTF8(size_desc, 0, x, text_y, LLColor4::purple); + } + // Leave a blank line + text_y -= (line_height * 2); + + // Everything below "header" + for(U32 line = first_line; line <= last_line; line++) + { + if(line >= line_count) break; + + // Offset on left + text = llformat("%08X", line * mColumns); // offset on left + mGLFont->renderUTF8(text, 0, text_x, text_y, LLColor4::blue); + + // Setup for rendering hex and ascii + U32 line_char_offset = mColumns * line; + U32 colstart0 = 0; + U32 colend0 = mColumns; + U32 colstart1 = mColumns; + U32 colend1 = mColumns; + U32 colstart2 = mColumns; + U32 colend2 = mColumns; + if(mHasSelection) + { + if(line == selection_first_line) + { + colend0 = selection_start_column; + colstart1 = selection_start_column; + if(selection_first_line == selection_last_line) + { + colend1 = selection_end_column; + colstart2 = selection_end_column; + colend2 = mColumns; + } + } + else if(line > selection_first_line && line < selection_last_line) + { + colend0 = 0; + colstart1 = 0; + colend1 = mColumns; + } + else if(line == selection_last_line) + { + colend0 = 0; + colstart1 = 0; + colend1 = selection_end_column; + colstart2 = selection_end_column; + colend2 = mColumns; + } + } + + // Data in hex + text = ""; + for(U32 c = colstart0; c < colend0; c++) + { + U32 o = line_char_offset + c; + if(o >= data_length) text.append(" "); + else text.append(llformat(" %02X", mValue[o])); + } + mGLFont->renderUTF8(text, 0, text_x_data + (colstart0 * data_column_width), text_y, LLColor4::black); + text = ""; + for(U32 c = colstart1; c < colend1; c++) + { + U32 o = line_char_offset + c; + if(o >= data_length) text.append(" "); + else text.append(llformat(" %02X", mValue[o])); + } + mGLFont->renderUTF8(text, 0, text_x_data + (colstart1 * data_column_width), text_y, LLColor4::white); + text = ""; + for(U32 c = colstart2; c < colend2; c++) + { + U32 o = line_char_offset + c; + if(o >= data_length) text.append(" "); + else text.append(llformat(" %02X", mValue[o])); + } + mGLFont->renderUTF8(text, 0, text_x_data + (colstart2 * data_column_width), text_y, LLColor4::black); + + // ASCII + text = ""; + for(U32 c = colstart0; c < colend0; c++) + { + U32 o = line_char_offset + c; + if(o >= data_length) break; + if((mValue[o] < 0x20) || (mValue[o] >= 0x7F)) text.append("."); + else text.append(llformat("%c", mValue[o])); + } + mGLFont->renderUTF8(text, 0, text_x_ascii + (colstart0 * char_width), text_y, LLColor4::black); + text = ""; + for(U32 c = colstart1; c < colend1; c++) + { + U32 o = line_char_offset + c; + if(o >= data_length) break; + if((mValue[o] < 0x20) || (mValue[o] >= 0x7F)) text.append("."); + else text.append(llformat("%c", mValue[o])); + } + mGLFont->renderUTF8(text, 0, text_x_ascii + (colstart1 * char_width), text_y, LLColor4::white); + text = ""; + for(U32 c = colstart2; c < colend2; c++) + { + U32 o = line_char_offset + c; + if(o >= data_length) break; + if((mValue[o] < 0x20) || (mValue[o] >= 0x7F)) text.append("."); + else text.append(llformat("%c", mValue[o])); + } + mGLFont->renderUTF8(text, 0, text_x_ascii + (colstart2 * char_width), text_y, LLColor4::black); + + text_y -= line_height; + } + + + + // Cursor + if(has_focus && !mHasSelection && (U32(LLTimer::getElapsedSeconds() * 2.0f) & 0x1)) + { + U32 cursor_line = mCursorPos / mColumns; + if((cursor_line >= first_line) && (cursor_line <= last_line)) + { + F32 pixel_y = (F32)(mTextRect.mTop - line_height); + pixel_y -= line_height * (2 + (cursor_line - first_line)); + + U32 cursor_offset = mCursorPos % mColumns; // bytes + F32 pixel_x = mInData ? text_x_data : text_x_ascii; + if(mInData) + { + pixel_x += data_column_width * cursor_offset; + pixel_x += char_width; + if(mSecondNibble) pixel_x += char_width; + } + else + { + pixel_x += char_width * cursor_offset; + } + pixel_x -= 2.0f; + pixel_y -= 2.0f; + gl_rect_2d(pixel_x, pixel_y + line_height, pixel_x + 2, pixel_y, LLColor4::black); + } + } +} + +void LLHexEditor::deselect() +{ + mSelectionStart = mCursorPos; + mSelectionEnd = mCursorPos; + mHasSelection = FALSE; + mSelecting = FALSE; +} + +BOOL LLHexEditor::canUndo() const +{ + return mUndoBuffer->canUndo(); +} + +void LLHexEditor::undo() +{ + mUndoBuffer->undoAction(); +} + +BOOL LLHexEditor::canRedo() const +{ + return mUndoBuffer->canRedo(); +} + +void LLHexEditor::redo() +{ + mUndoBuffer->redoAction(); +} + + + + +void LLHexEditor::moveCursor(U32 pos, BOOL second_nibble) +{ + mCursorPos = pos; + + // Clamp and handle second nibble + if(mCursorPos >= mValue.size()) + { + mCursorPos = mValue.size(); + mSecondNibble = FALSE; + } + else + { + mSecondNibble = mInData ? second_nibble : FALSE; + } + + // Change selection + mSelectionEnd = mCursorPos; + if(!mHasSelection) mSelectionStart = mCursorPos; + + // Scroll + U32 line = mCursorPos / mColumns; + U32 first_line = mScrollbar->getDocPos(); + U32 last_line = first_line + mScrollbar->getPageSize(); // don't -2 from scrollbar sizes + if(line < first_line) mScrollbar->setDocPos(line); + if(line > (last_line - 2)) mScrollbar->setDocPos(line - mScrollbar->getPageSize() + 1); +} + +BOOL LLHexEditor::canCut() const +{ + return mHasSelection; +} + +void LLHexEditor::cut() +{ + if(!canCut()) return; + + copy(); + + U32 start = getProperSelectionStart(); + del(start, getProperSelectionEnd() - 1, TRUE); + + moveCursor(start, FALSE); +} + +BOOL LLHexEditor::canCopy() const +{ + return mHasSelection; +} + +void LLHexEditor::copy() +{ + if(!canCopy()) return; + + std::string text; + if(mInData) + { + U32 start = getProperSelectionStart(); + U32 end = getProperSelectionEnd(); + for(U32 i = start; i < end; i++) + text.append(llformat("%02X", mValue[i])); + } + else + { + U32 start = getProperSelectionStart(); + U32 end = getProperSelectionEnd(); + for(U32 i = start; i < end; i++) + text.append(llformat("%c", mValue[i])); + } + LLWString wtext = utf8str_to_wstring(text); + LLClipboard::instance().copyToClipboard(wtext, 0, wtext.length()); +} + +BOOL LLHexEditor::canPaste() const +{ + return TRUE; +} + +void LLHexEditor::paste() +{ + if(!canPaste()) return; + + LLWString paste; + LLClipboard::instance().pasteFromClipboard(paste, true); + + std::string clipstr = wstring_to_utf8str(paste);//wstring_to_utf8str(LLClipboard::instance().getPasteWString()); + const char* clip = clipstr.c_str(); + + std::vector<U8> new_data; + if(mInData) + { + size_t len = strlen(clip); + for(size_t i = 0; (i + 1) < len; i += 2) + { + S32 c = 0; + if(sscanf(&(clip[i]), "%02X", &c) != 1) break; + new_data.push_back(U8(c)); + } + } + else + { + size_t len = strlen(clip); + for(size_t i = 0; i < len; ++i) + { + U8 c = 0; + if(sscanf(&(clip[i]), "%c", &c) != 1) break; + new_data.push_back(c); + } + } + + U32 start = mCursorPos; + if(!mHasSelection) + insert(start, new_data, TRUE); + else + { + start = getProperSelectionStart(); + overwrite(start, getProperSelectionEnd() - 1, new_data, TRUE); + } + + moveCursor(start + new_data.size(), FALSE); +} + +BOOL LLHexEditor::canDoDelete() const +{ + return mValue.size() > 0; +} + +void LLHexEditor::doDelete() +{ + if(!canDoDelete()) return; + + U32 start = getProperSelectionStart(); + del(start, getProperSelectionEnd(), TRUE); + + moveCursor(start, FALSE); +} + +BOOL LLHexEditor::canSelectAll() const +{ + return mValue.size() > 0; +} + +void LLHexEditor::selectAll() +{ + if(!canSelectAll()) return; + + mSelectionStart = 0; + mSelectionEnd = mValue.size(); + mHasSelection = mSelectionStart != mSelectionEnd; +} + +BOOL LLHexEditor::canDeselect() const +{ + return mHasSelection; +} + +void LLHexEditor::insert(U32 pos, std::vector<U8> new_data, BOOL undoable) +{ + if(pos > mValue.size()) + { + LL_WARNS() << "pos outside data!" << LL_ENDL; + return; + } + + deselect(); + + if(undoable) + { + LLUndoHex* action = (LLUndoHex*)(mUndoBuffer->getNextAction()); + action->set(this, &(LLUndoHex::undoInsert), &(LLUndoHex::redoInsert), pos, pos, std::vector<U8>(), new_data); + } + + std::vector<U8>::iterator wheres = mValue.begin() + pos; + + mValue.insert(wheres, new_data.begin(), new_data.end()); + + changedLength(); +} + +void LLHexEditor::overwrite(U32 first_pos, U32 last_pos, std::vector<U8> new_data, BOOL undoable) +{ + if(first_pos > mValue.size() || last_pos > mValue.size()) + { + LL_WARNS() << "pos outside data!" << LL_ENDL; + return; + } + + deselect(); + + std::vector<U8>::iterator first = mValue.begin() + first_pos; + std::vector<U8>::iterator last = mValue.begin() + last_pos; + + std::vector<U8> old_data; + if(last_pos > 0) old_data = std::vector<U8>(first, last + 1); + + if(undoable) + { + LLUndoHex* action = (LLUndoHex*)(mUndoBuffer->getNextAction()); + action->set(this, &(LLUndoHex::undoOverwrite), &(LLUndoHex::redoOverwrite), first_pos, last_pos, old_data, new_data); + } + + mValue.erase(first, last + 1); + first = mValue.begin() + first_pos; + mValue.insert(first, new_data.begin(), new_data.end()); + + changedLength(); +} + +void LLHexEditor::del(U32 first_pos, U32 last_pos, BOOL undoable) +{ + if(first_pos > mValue.size() || last_pos > mValue.size()) + { + LL_WARNS() << "pos outside data!" << LL_ENDL; + return; + } + + deselect(); + + std::vector<U8>::iterator first = mValue.begin() + first_pos; + std::vector<U8>::iterator last = mValue.begin() + last_pos; + + std::vector<U8> old_data; + if(last_pos > 0) old_data = std::vector<U8>(first, last + 1); + + if(undoable) + { + LLUndoHex* action = (LLUndoHex*)(mUndoBuffer->getNextAction()); + action->set(this, &(LLUndoHex::undoDel), &(LLUndoHex::redoDel), first_pos, last_pos, old_data, std::vector<U8>()); + } + + mValue.erase(first, last + 1); + + changedLength(); +} + +void LLUndoHex::set(LLHexEditor* hex_editor, + void (*undo_action)(LLUndoHex*), + void (*redo_action)(LLUndoHex*), + U32 first_pos, + U32 last_pos, + std::vector<U8> old_data, + std::vector<U8> new_data) +{ + mHexEditor = hex_editor; + mUndoAction = undo_action; + mRedoAction = redo_action; + mFirstPos = first_pos; + mLastPos = last_pos; + mOldData = old_data; + mNewData = new_data; +} + +void LLUndoHex::undo() +{ + mUndoAction(this); +} + +void LLUndoHex::redo() +{ + mRedoAction(this); +} + +void LLUndoHex::undoInsert(LLUndoHex* action) +{ + //action->mHexEditor->del(action->mFirstPos, action->mLastPos, FALSE); + action->mHexEditor->del(action->mFirstPos, action->mFirstPos + action->mNewData.size() - 1, FALSE); +} + +void LLUndoHex::redoInsert(LLUndoHex* action) +{ + action->mHexEditor->insert(action->mFirstPos, action->mNewData, FALSE); +} + +void LLUndoHex::undoOverwrite(LLUndoHex* action) +{ + //action->mHexEditor->overwrite(action->mFirstPos, action->mLastPos, action->mOldData, FALSE); + action->mHexEditor->overwrite(action->mFirstPos, action->mFirstPos + action->mNewData.size() - 1, action->mOldData, FALSE); +} + +void LLUndoHex::redoOverwrite(LLUndoHex* action) +{ + action->mHexEditor->overwrite(action->mFirstPos, action->mLastPos, action->mNewData, FALSE); +} + +void LLUndoHex::undoDel(LLUndoHex* action) +{ + action->mHexEditor->insert(action->mFirstPos, action->mOldData, FALSE); +} + +void LLUndoHex::redoDel(LLUndoHex* action) +{ + action->mHexEditor->del(action->mFirstPos, action->mLastPos, FALSE); +} + + +// </edit> diff --git a/indra/newview/llhexeditor.h b/indra/newview/llhexeditor.h new file mode 100644 index 00000000000..9ff06d1e614 --- /dev/null +++ b/indra/newview/llhexeditor.h @@ -0,0 +1,163 @@ +/** + * @file dohexeditor.h + * @brief DOHexEditor Widget + * @author Day Oh, Skills, Cinder + * + * $LicenseInfo:firstyear=2009&license=WTFPLV2$ + * + */ + +#ifndef LLHexEditor_H +#define LLHexEditor_H + +#define MIN_COLS 8 +#define MAX_COLS 48 + +#ifndef COLUMN_SPAN +#define COLUMN_SPAN +#endif + +#include "lluictrl.h" +#include "llscrollbar.h" +#include "llviewborder.h" +#include "llundo.h" +#include "lleditmenuhandler.h" + +class LLHexEditor : public LLUICtrl, public LLEditMenuHandler +{ +public: + + struct Params : LLInitParam::Block<Params, LLUICtrl::Params> + { }; + + ~LLHexEditor() override; + void setValue(const LLSD& value); + LLSD getValue() const; + void setColumns(U8 columns); + U8 getColumns() { return mColumns; }; + U32 getLineCount() const; + F32 getSuggestedWidth(U8 cols = -1); + U32 getProperSelectionStart() const; + U32 getProperSelectionEnd() const; + void reshape(S32 width, S32 height, BOOL called_from_parent); + void setFocus(BOOL b); + + BOOL handleScrollWheel(S32 x, S32 y, S32 clicks) const; + BOOL handleMouseDown(S32 x, S32 y, MASK mask); + BOOL handleHover(S32 x, S32 y, MASK mask); + BOOL handleMouseUp(S32 x, S32 y, MASK mask); + + BOOL handleKeyHere(KEY key, MASK mask); + BOOL handleKey(KEY key, MASK mask, BOOL called_from_parent); + BOOL handleUnicodeChar(llwchar uni_char, BOOL called_from_parent); + BOOL handleUnicodeCharHere(llwchar uni_char); + + /*virtual*/ BOOL postBuild(); + void draw(); + + void moveCursor(U32 pos, BOOL second_nibble); + + void insert(U32 pos, std::vector<U8> new_data, BOOL undoable); + void overwrite(U32 first_pos, U32 last_pos, std::vector<U8> new_data, BOOL undoable); + void del(U32 first_pos, U32 last_pos, BOOL undoable); + + virtual void cut() override; + virtual BOOL canCut() const override; + + virtual void copy() override; + virtual BOOL canCopy() const override; + + virtual void paste() override; + virtual BOOL canPaste() const override; + + virtual void doDelete() override; + virtual BOOL canDoDelete() const override; + + virtual void selectAll() override; + virtual BOOL canSelectAll() const override; + + virtual void deselect() override; + virtual BOOL canDeselect() const override; + + virtual void undo() override; + virtual BOOL canUndo() const override; + + virtual void redo() override; + virtual BOOL canRedo() const override; + +private: + std::vector<U8> mValue; + U8 mColumns; + + std::string mName; + U32 mCursorPos; + BOOL mSecondNibble; + BOOL mInData; + BOOL mSelecting; + BOOL mHasSelection; + U32 mSelectionStart; + U32 mSelectionEnd; + + LLFontGL* mGLFont; + LLRect mTextRect; + LLScrollbar* mScrollbar; + LLViewBorder* mBorder; + + LLUndoBuffer* mUndoBuffer; + + void changedLength(); + void getPosAndContext(S32 x, S32 y, BOOL force_context, U32& pos, BOOL& in_data, BOOL& second_nibble) const; +protected: + LLHexEditor(const Params & p); + friend class LLUICtrlFactory; +}; + +class LLUndoHex : public LLUndoBuffer::LLUndoAction +{ +protected: + LLHexEditor* mHexEditor = nullptr; + U32 mFirstPos = 0; + U32 mLastPos = 0; + std::vector<U8> mOldData; + std::vector<U8> mNewData; +public: + static LLUndoAction* create() { return new LLUndoHex(); } + virtual void set(LLHexEditor* hex_editor, + void (*undo_action)(LLUndoHex*), + void (*redo_action)(LLUndoHex*), + U32 first_pos, + U32 last_pos, + std::vector<U8> old_data, + std::vector<U8> new_data); + void (*mUndoAction)(LLUndoHex*); + void (*mRedoAction)(LLUndoHex*); + virtual void undo() override; + virtual void redo() override; + + static void undoInsert(LLUndoHex* action); + static void redoInsert(LLUndoHex* action); + static void undoOverwrite(LLUndoHex* action); + static void redoOverwrite(LLUndoHex* action); + static void undoDel(LLUndoHex* action); + static void redoDel(LLUndoHex* action); +}; + +class LLHexInsert : public LLUndoHex +{ + virtual void undo() override; + virtual void redo() override; +}; + +class LLHexOverwrite : public LLUndoHex +{ + virtual void undo() override; + virtual void redo() override; +}; + +class LLHexDel : public LLUndoHex +{ + virtual void undo() override; + virtual void redo() override; +}; + +#endif // LLHexEditor_H diff --git a/indra/newview/llinventorybridge.cpp b/indra/newview/llinventorybridge.cpp index 1137d0da82b..dc3d4b0c92c 100644 --- a/indra/newview/llinventorybridge.cpp +++ b/indra/newview/llinventorybridge.cpp @@ -917,6 +917,17 @@ void LLInvFVBridge::getClipboardEntries(bool show_asset_id, } } } + static LLCachedControl<bool> sPowerfulWizard(gSavedSettings, "AlchemyPowerfulWizard", false); + if (show_asset_id && sPowerfulWizard) + { + items.push_back(LLStringExplicit("Extras Separator")); + items.push_back(LLStringExplicit("Extras Menu")); + + if (!isItemModifyable()) + { + disabled_items.push_back(LLStringExplicit("Edit Hex")); + } + } } // [SL:KB] - Patch: Inventory-Actions | Checked: 2010-04-12 (Catznip-2.0) @@ -1836,6 +1847,15 @@ void LLItemBridge::performAction(LLInventoryModel* model, std::string action) std::string url = LLMarketplaceData::instance().getListingURL(mUUID); LLUrlAction::openURL(url); } + else if ("edit_hex" == action) + { + LLInventoryItem* item = model->getItem(mUUID); + if (!item) { return; } + + LLFloaterReg::showInstance("asset_hex_editor", + LLSD().with("inv_id", item->getUUID()) + .with("asset_type", item->getActualType())); + } } void LLItemBridge::doActionOnCurSelectedLandmark(LLLandmarkList::loaded_callback_t cb) @@ -2242,6 +2262,16 @@ BOOL LLItemBridge::isItemCopyable() const return FALSE; } +BOOL LLItemBridge::isItemModifyable() const +{ + LLViewerInventoryItem* item = getItem(); + if (item) + { + return (item->getPermissions().allowModifyBy(gAgent.getID())); + } + return FALSE; +} + // [SL:KB] - Patch: Inventory-Links | Checked: 2013-09-19 (Catznip-3.6) bool LLItemBridge::isItemLinkable() const { diff --git a/indra/newview/llinventorybridge.h b/indra/newview/llinventorybridge.h index 7e22a7afb5e..17614137995 100644 --- a/indra/newview/llinventorybridge.h +++ b/indra/newview/llinventorybridge.h @@ -120,6 +120,7 @@ class LLInvFVBridge : public LLFolderViewModelItemInventory virtual void removeBatch(std::vector<LLFolderViewModelItem*>& batch); virtual void move(LLFolderViewModelItem* new_parent_bridge) {} virtual BOOL isItemCopyable() const { return FALSE; } + virtual BOOL isItemModifyable() const { return FALSE; } // [SL:KB] - Patch: Inventory-Links | Checked: 2013-09-19 (Catznip-3.6) virtual bool isItemLinkable() const { return FALSE; } // [/SL:KB] @@ -249,6 +250,7 @@ class LLItemBridge : public LLInvFVBridge virtual BOOL renameItem(const std::string& new_name); virtual BOOL removeItem(); virtual BOOL isItemCopyable() const; + virtual BOOL isItemModifyable() const; // [SL:KB] - Patch: Inventory-Links | Checked: 2013-09-19 (Catznip-3.6) /*virtual*/ bool isItemLinkable() const; // [/SL:KB] diff --git a/indra/newview/llviewerfloaterreg.cpp b/indra/newview/llviewerfloaterreg.cpp index 1e669af88cc..bdb25056ad7 100644 --- a/indra/newview/llviewerfloaterreg.cpp +++ b/indra/newview/llviewerfloaterreg.cpp @@ -91,6 +91,7 @@ #include "llfloatergridstatus.h" #include "llfloatergroups.h" #include "llfloaterhelpbrowser.h" +#include "llfloaterhexeditor.h" #include "llfloaterhoverheight.h" #include "llfloaterhowto.h" #include "llfloaterhud.h" @@ -424,6 +425,7 @@ void LLViewerFloaterReg::registerFloaters() // *NOTE: Please keep these alphabetized for easier merges LLFloaterReg::add("ao", "floater_ao.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<ALFloaterAO>); + LLFloaterReg::add("asset_hex_editor", "floater_hex_editor.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterHexEditor>); LLFloaterReg::add("delete_queue", "floater_script_queue.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterDeleteQueue>); LLFloaterReg::add("generic_text", "floater_generic_text.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterGenericText>); LLFloaterReg::add("lightbox", "floater_lightbox_settings.xml", (LLFloaterBuildFunc) &LLFloaterReg::build<ALFloaterLightBox>); diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index ca544f94b4b..0799fea3167 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -2442,6 +2442,53 @@ void LLViewerFetchedTexture::setLoadedCallback( loaded_callback_func loaded_call mLastReferencedSavedRawImageTime = sCurrentTime; } +void LLViewerFetchedTexture::setLoadedCallbackNoAux(loaded_callback_func loaded_callback, S32 discard_level, BOOL keep_imageraw, + BOOL needs_aux, void* userdata, + LLLoadedCallbackEntry::source_callback_list_t* src_callback_list, BOOL pause) +{ + // + // Don't do ANYTHING here, just add it to the global callback list + // + if (mLoadedCallbackList.empty()) + { + // Put in list to call this->doLoadedCallbacks() periodically + gTextureList.mCallbackList.insert(this); + mLoadedCallbackDesiredDiscardLevel = (S8)discard_level; + } + else + { + mLoadedCallbackDesiredDiscardLevel = llmin(mLoadedCallbackDesiredDiscardLevel, (S8) discard_level); + } + + if (mPauseLoadedCallBacks) + { + if (!pause) + { + unpauseLoadedCallbacks(src_callback_list); + } + } + else if (pause) + { + pauseLoadedCallbacks(src_callback_list); + } + + LLLoadedCallbackEntry* entryp = + new LLLoadedCallbackEntry(loaded_callback, discard_level, keep_imageraw, userdata, src_callback_list, this, pause); + mLoadedCallbackList.push_back(entryp); + + mNeedsAux = needs_aux; + if (keep_imageraw) + { + mSaveRawImage = TRUE; + } + if (mNeedsAux && mAuxRawImage.isNull() && getDiscardLevel() >= 0) + { + // We need aux data, but we've already loaded the image, and it didn't have any + LL_WARNS() << "No aux data available for callback for image:" << getID() << LL_ENDL; + } + mLastCallBackActiveTime = sCurrentTime; +} + void LLViewerFetchedTexture::clearCallbackEntryList() { if(mLoadedCallbackList.empty()) diff --git a/indra/newview/llviewertexture.h b/indra/newview/llviewertexture.h index e430746585d..1bb16758892 100644 --- a/indra/newview/llviewertexture.h +++ b/indra/newview/llviewertexture.h @@ -311,6 +311,8 @@ class LLViewerFetchedTexture : public LLViewerTexture void setLoadedCallback(loaded_callback_func cb, S32 discard_level, BOOL keep_imageraw, BOOL needs_aux, void* userdata, LLLoadedCallbackEntry::source_callback_list_t* src_callback_list, BOOL pause = FALSE); + void setLoadedCallbackNoAux(loaded_callback_func cb, S32 discard_level, BOOL keep_imageraw, BOOL needs_aux, void* userdata, + LLLoadedCallbackEntry::source_callback_list_t* src_callback_list, BOOL pause = FALSE); bool hasCallbacks() { return mLoadedCallbackList.empty() ? false : true; } void pauseLoadedCallbacks(const LLLoadedCallbackEntry::source_callback_list_t* callback_list); void unpauseLoadedCallbacks(const LLLoadedCallbackEntry::source_callback_list_t* callback_list); diff --git a/indra/newview/skins/default/xui/en/floater_hex_editor.xml b/indra/newview/skins/default/xui/en/floater_hex_editor.xml new file mode 100644 index 00000000000..549856900e3 --- /dev/null +++ b/indra/newview/skins/default/xui/en/floater_hex_editor.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<floater can_close="true" can_drag_on_left="false" can_minimize="true" + can_resize="true" height="197" width="580" min_width="580" min_height="128" + name="asset_hex_editor" title="Hex Editor" save_rect="true" positioning="cascading"> + <hex_editor name="hex" follows="left|top|right|bottom" left="5" top="5" width="570" height="170" visible="true"/> + <slider can_edit_text="false" top_pad="0" control_name="column_count" + decimal_digits="3" follows="left|bottom" height="16" increment="1" + label="Columns" left="10" max_val="48" min_val="8" + mouse_opaque="true" name="column_count" show_text="false" value="16" + width="100" /> + <text name="status_text" follows="left|bottom" width="300" left="120" top_delta="0" height="20">[STATUS]</text> + <button name="upload_btn" follows="right|bottom" right="-120" width="100" top_delta="-5" label="[UPLOAD]" enabled="false"/> + <button name="save_btn" follows="right|bottom" right="-10" width="100" top_delta="0" label="Save" enabled="false"/> +</floater> diff --git a/indra/newview/skins/default/xui/en/menu_inventory.xml b/indra/newview/skins/default/xui/en/menu_inventory.xml index cf57268f72f..e4194557297 100644 --- a/indra/newview/skins/default/xui/en/menu_inventory.xml +++ b/indra/newview/skins/default/xui/en/menu_inventory.xml @@ -931,6 +931,21 @@ function="Inventory.DoToSelected" parameter="move_to_marketplace_listings" /> </menu_item_call> + <menu_item_separator + layout="topleft" + name="Extras Separator" /> + <menu + label="Extras" + layout="topleft" + name="Extras Menu"> + <menu_item_call + label="Edit Hex" + name="Edit Hex"> + <on_click + function="Inventory.DoToSelected" + parameter="edit_hex" /> + </menu_item_call> + </menu> <menu_item_call label="--no options--" layout="topleft" -- GitLab