From 407c461473049942edf56e6e8f561f057354c581 Mon Sep 17 00:00:00 2001 From: Rye Mutt <rye@alchemyviewer.org> Date: Mon, 23 Mar 2020 21:11:22 -0400 Subject: [PATCH] Add skinning package support back along with skin selection --- indra/llvfs/lldir.cpp | 1 + indra/newview/CMakeLists.txt | 3 + indra/newview/alunzip.cpp | 231 ++++++++++++ indra/newview/alunzip.h | 74 ++++ indra/newview/llfilepicker.cpp | 17 + indra/newview/llfilepicker.h | 3 +- indra/newview/llfloaterpreference.cpp | 334 ++++++++++++++++-- indra/newview/llfloaterpreference.h | 18 +- indra/newview/skins/default/manifest.json | 6 + .../default/xui/en/floater_preferences.xml | 5 + .../skins/default/xui/en/notifications.xml | 56 +++ .../xui/en/panel_preferences_skins.xml | 203 +++++++++++ .../newview/skins/default/xui/en/strings.xml | 1 + indra/newview/viewer_manifest.py | 1 + 14 files changed, 912 insertions(+), 41 deletions(-) create mode 100644 indra/newview/alunzip.cpp create mode 100644 indra/newview/alunzip.h create mode 100644 indra/newview/skins/default/manifest.json create mode 100644 indra/newview/skins/default/xui/en/panel_preferences_skins.xml diff --git a/indra/llvfs/lldir.cpp b/indra/llvfs/lldir.cpp index 00bcb32253a..54babb6ad9a 100644 --- a/indra/llvfs/lldir.cpp +++ b/indra/llvfs/lldir.cpp @@ -1051,6 +1051,7 @@ void LLDir::dumpCurrentDirectories(LLError::ELevel level) LL_VLOGS(level, "AppInit", "Directories") << " CAFile: " << getCAFile() << LL_ENDL; LL_VLOGS(level, "AppInit", "Directories") << " SkinBaseDir: " << getSkinBaseDir() << LL_ENDL; LL_VLOGS(level, "AppInit", "Directories") << " SkinDir: " << getSkinDir() << LL_ENDL; + LL_VLOGS(level, "AppInit", "Directories") << " UserSkinDir: " << getUserSkinDir() << LL_ENDL; } void LLDir::append(std::string& destpath, const std::string& name) const diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 0b5bf08097b..8f47dcfad2d 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -68,6 +68,7 @@ endif(USE_FMODSTUDIO) include_directories( ${DBUSGLIB_INCLUDE_DIRS} + ${ZLIB_INCLUDE_DIRS} ${JSONCPP_INCLUDE_DIR} ${GLOD_INCLUDE_DIR} ${LLAUDIO_INCLUDE_DIRS} @@ -109,6 +110,7 @@ include_directories(SYSTEM ) set(viewer_SOURCE_FILES + alunzip.cpp groupchatlistener.cpp llaccountingcostmanager.cpp llaisapi.cpp @@ -743,6 +745,7 @@ set(VIEWER_BINARY_NAME "secondlife-bin" CACHE STRING set(viewer_HEADER_FILES CMakeLists.txt ViewerInstall.cmake + alunzip.h groupchatlistener.h llaccountingcost.h llaccountingcostmanager.h diff --git a/indra/newview/alunzip.cpp b/indra/newview/alunzip.cpp new file mode 100644 index 00000000000..195d29730d8 --- /dev/null +++ b/indra/newview/alunzip.cpp @@ -0,0 +1,231 @@ +/** + * @file alunzip.cpp + * @brief Minizip wrapper + * + * Copyright (c) 2015, Cinder Roxley <cinder@sdf.org> + * + * Permission is hereby granted, free of charge, to any person or organization + * obtaining a copy of the software and accompanying documentation covered by + * this license (the "Software") to use, reproduce, display, distribute, + * execute, and transmit the Software, and to prepare derivative works of the + * Software, and to permit third-parties to whom the Software is furnished to + * do so, all subject to the following: + * + * The copyright notices in the Software and this entire statement, including + * the above license grant, this restriction and the following disclaimer, + * must be included in all copies of the Software, in whole or in part, and + * all derivative works of the Software, unless such copies or derivative + * works are solely in the form of machine-executable object code generated by + * a source language processor. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT + * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE + * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +#include "llviewerprecompiledheaders.h" +#include "alunzip.h" + +S32 CASE_SENTITIVITY = 1; +size_t WRITE_BUFFER_SIZE = 8192; + +ALUnZip::ALUnZip(const std::string& filename) +: mFilename(filename) +, mValid(false) +{ + mZipfile = open(filename); + mValid = (mZipfile != nullptr); +} + +ALUnZip::~ALUnZip() +{ + close(); +} + +bool ALUnZip::extract(const std::string& path) +{ + S32 error = UNZ_OK; + unz_global_info64 gi; + + unzGoToFirstFile(mZipfile); + + error = unzGetGlobalInfo64(mZipfile, &gi); + if (error != UNZ_OK) + { + LL_WARNS("ALUNZIP") << "Error unzipping " << mFilename << " - code: " << error << LL_ENDL; + return false; + } + + for (uLong i = 0; i < gi.number_entry ; i++) + { + if (extractCurrentFile(path) != UNZ_OK) + break; + if ((i + 1) < gi.number_entry) + { + error = unzGoToNextFile(mZipfile); + if (error != UNZ_OK) + { + LL_WARNS("ALUNZIP") << "Error unzipping " << mFilename << " - code: " << error << LL_ENDL; + break; + } + } + } + + return true; +} + +S32 ALUnZip::extractCurrentFile(const std::string& path) +{ + S32 error = UNZ_OK; + char filename_inzip[256]; + unz_file_info64 file_info; + + error = unzGetCurrentFileInfo64(mZipfile, &file_info, filename_inzip, sizeof(filename_inzip), nullptr, 0, nullptr, 0); + if (error != UNZ_OK) + { + LL_WARNS("ALUNZIP") << "Error unzipping " << mFilename << " - code: " << error << LL_ENDL; + return error; + } + + const std::string& inzip(filename_inzip); + const std::string& write_filename = gDirUtilp->add(path, inzip); + + LL_INFOS("ALUNZIP") << "Unpacking " << inzip << LL_ENDL; + if (LLStringUtil::endsWith(inzip, "/") || LLStringUtil::endsWith(inzip, "\\")) + { + if (!LLFile::isdir(write_filename)) + { + if (LLFile::mkdir(write_filename) != 0) + LL_WARNS("ALUNZIP") << "Couldn't create directory at " << write_filename << LL_ENDL; + } + return error; + } + + size_t size_buf = WRITE_BUFFER_SIZE; + auto buf = std::make_unique<char[]>(size_buf); + if (buf == nullptr) + { + LL_WARNS("ALUNZIP") << "Error allocating memory!" << LL_ENDL; + return UNZ_INTERNALERROR; + } + + error = unzOpenCurrentFile(mZipfile); + if (error != UNZ_OK) + { + LL_WARNS("ALUNZIP") << "Error unzipping " << mFilename << " - code: " << error << LL_ENDL; + } + + LLFILE* outfile = LLFile::fopen(write_filename, "wb"); + if (outfile == nullptr) + { + LL_WARNS("ALUNZIP") << "Error opening " << write_filename << " for writing" << LL_ENDL; + } + else + { + do + { + error = unzReadCurrentFile(mZipfile, buf.get(), size_buf); + if (error < UNZ_OK) + { + LL_WARNS("ALUNZIP") << "Error unzipping " << mFilename << " - code: " << error << LL_ENDL; + break; + } + else if (error > UNZ_OK) + { + if (fwrite(buf.get(), error, 1, outfile) != 1) + { + LL_WARNS("ALUNZIP") << "Error writing out " << mFilename << LL_ENDL; + error = UNZ_ERRNO; + break; + } + } + } while (error > 0); + fclose(outfile); + } + + + if (error == UNZ_OK) + { + error = unzCloseCurrentFile(mZipfile); + if (error != UNZ_OK) + LL_WARNS("ALUNZIP") << "Error unzipping " << mFilename << " - code: " << error << LL_ENDL; + } + else + unzCloseCurrentFile(mZipfile); + + return error; +} + +bool ALUnZip::extractFile(const std::string& file_to_extract, char *buf, size_t bufsize) +{ + if (unzLocateFile(mZipfile, file_to_extract.c_str(), CASE_SENTITIVITY) != UNZ_OK) + { + LL_WARNS("ALUNZIP") << file_to_extract << " was not found in " << mFilename << LL_ENDL; + return false; + } + + S32 error = UNZ_OK; + + char filename_inzip[256]; + unz_file_info64 file_info; + error = unzGetCurrentFileInfo64(mZipfile, &file_info, filename_inzip, sizeof(filename_inzip), nullptr, 0, nullptr, 0); + error = unzOpenCurrentFile(mZipfile); + error = unzReadCurrentFile(mZipfile, buf, bufsize); + unzCloseCurrentFile(mZipfile); + if (error != UNZ_OK) + { + LL_WARNS("ALUNZIP") << "Error unzipping " << mFilename << " - code: " << error << LL_ENDL; + return false; + } + + return true; +} + +size_t ALUnZip::getSizeFile(const std::string& file_to_size) +{ + if (unzLocateFile(mZipfile, file_to_size.c_str(), CASE_SENTITIVITY) != UNZ_OK) + { + LL_WARNS("ALUNZIP") << file_to_size << " was not found in " << mFilename << LL_ENDL; + return 0; + } + + S32 error = UNZ_OK; + char filename_inzip[256]; + unz_file_info64 file_info; + error = unzGetCurrentFileInfo64(mZipfile, &file_info, filename_inzip, sizeof(filename_inzip), nullptr, 0, nullptr, 0); + if (error != UNZ_OK) + { + LL_WARNS("ALUNZIP") << "Error unzipping " << mFilename << " - code: " << error << LL_ENDL; + return 0; + } + return file_info.uncompressed_size; +} + +unzFile ALUnZip::open(const std::string& filename) +{ +#ifdef USEWIN32IOAPI + zlib_filefunc64_def ffunc; + + fill_win32_filefunc64A(&ffunc); + mZipfile = unzOpen2_64(filename.c_str(), &ffunc); +#else // !USEWIN32IOAPI + mZipfile = unzOpen64(filename.c_str()); +#endif // !USEWIN32IOAPI + + if (mZipfile == nullptr) + LL_WARNS("ALUNZIP") << "Failed to open " << mFilename << LL_ENDL; + + return mZipfile; +} + +void ALUnZip::close() +{ + unzClose(mZipfile); + mZipfile = nullptr; + mValid = false; +} diff --git a/indra/newview/alunzip.h b/indra/newview/alunzip.h new file mode 100644 index 00000000000..59aa8b9a264 --- /dev/null +++ b/indra/newview/alunzip.h @@ -0,0 +1,74 @@ +/** + * @file alunzip.h + * @brief Minizip wrapper + * + * Copyright (c) 2015, Cinder Roxley <cinder@sdf.org> + * + * Permission is hereby granted, free of charge, to any person or organization + * obtaining a copy of the software and accompanying documentation covered by + * this license (the "Software") to use, reproduce, display, distribute, + * execute, and transmit the Software, and to prepare derivative works of the + * Software, and to permit third-parties to whom the Software is furnished to + * do so, all subject to the following: + * + * The copyright notices in the Software and this entire statement, including + * the above license grant, this restriction and the following disclaimer, + * must be included in all copies of the Software, in whole or in part, and + * all derivative works of the Software, unless such copies or derivative + * works are solely in the form of machine-executable object code generated by + * a source language processor. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT + * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE + * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef AL_UNZIP_H +#define AL_UNZIP_H + +extern "C" +{ +#include <minizip/unzip.h> +} + +class ALUnZip +{ +public: + ALUnZip(const std::string& filename); + ~ALUnZip(); + + /* =============================================================== + * @name Archive manipulation functions + * @{ + */ + + /// Extracts the archive to a filesystem path + bool extract(const std::string& path); + + /// Extracts a single file from the archive into a memory buffer + bool extractFile(const std::string& file_to_extract, char *buf, size_t bufsize); + + /// Returns the uncompressed size of a file within the archive + size_t getSizeFile(const std::string& file_to_size); + + /// Returns true if the archive is valid + bool isValid() { return mValid; } + + //@} + +private: + unzFile open(const std::string& filename); + void close(); + S32 extractCurrentFile(const std::string& path); + + unzFile mZipfile; + std::string mFilename; + bool mValid; +}; + +#endif // LL_UNZIP_H diff --git a/indra/newview/llfilepicker.cpp b/indra/newview/llfilepicker.cpp index b6fd70452e7..919e2337ef4 100644 --- a/indra/newview/llfilepicker.cpp +++ b/indra/newview/llfilepicker.cpp @@ -64,6 +64,7 @@ LLFilePicker LLFilePicker::sInstance; #define MODEL_FILTER L"Model files (*.dae)\0*.dae\0" #define SCRIPT_FILTER L"Script files (*.lsl)\0*.lsl\0" #define DICTIONARY_FILTER L"Dictionary files (*.dic; *.xcu)\0*.dic;*.xcu\0" +#define ZIP_FILTER L"ZIP files (*.zip)\0*.zip\0" #endif #ifdef LL_DARWIN @@ -227,6 +228,10 @@ BOOL LLFilePicker::setupFilter(ELoadFilter filter) mOFN.lpstrFilter = DICTIONARY_FILTER \ L"\0"; break; + case FFLOAD_ZIP: + mOFN.lpstrFilter = ZIP_FILTER \ + L"\0"; + break; default: res = FALSE; break; @@ -642,6 +647,9 @@ std::vector<std::string>* LLFilePicker::navOpenFilterProc(ELoadFilter filter) // allowedv->push_back("dic"); allowedv->push_back("xcu"); break; + case FFLOAD_ZIP: + allowedv.push_back("zip"); + break; case FFLOAD_DIRECTORY: break; default: @@ -1188,6 +1196,12 @@ static std::string add_dictionary_filter_to_gtkchooser(GtkWindow *picker) LLTrans::getString("dictionary_files") + " (*.dic; *.xcu)"); } +static std::string add_zip_filter_to_gtkchooser(GtkWindow *picker) +{ + return add_simple_mime_filter_to_gtkchooser(picker, HTTP_CONTENT_TEXT_PLAIN, + LLTrans::getString("zip_files") + " (*.zip)"); +} + static std::string add_save_texture_filter_to_gtkchooser(GtkWindow *picker) { GtkFileFilter *gfilter_tga = gtk_file_filter_new(); @@ -1366,6 +1380,9 @@ BOOL LLFilePicker::getOpenFile( ELoadFilter filter, bool blocking ) case FFLOAD_DICTIONARY: filtername = add_dictionary_filter_to_gtkchooser(picker); break; + case FFLOAD_ZIP: + filtername = add_zip_filter_to_gtkchooser(picker); + break; default:; break; } diff --git a/indra/newview/llfilepicker.h b/indra/newview/llfilepicker.h index 2fc496a144a..c1fa147d2d7 100644 --- a/indra/newview/llfilepicker.h +++ b/indra/newview/llfilepicker.h @@ -88,7 +88,8 @@ class LLFilePicker FFLOAD_SCRIPT = 11, FFLOAD_DICTIONARY = 12, FFLOAD_DIRECTORY = 13, // To call from lldirpicker. - FFLOAD_EXE = 14 // Note: EXE will be treated as ALL on Windows and Linux but not on Darwin + FFLOAD_EXE = 14, // Note: EXE will be treated as ALL on Windows and Linux but not on Darwin + FFLOAD_ZIP = 15 }; enum ESaveFilter diff --git a/indra/newview/llfloaterpreference.cpp b/indra/newview/llfloaterpreference.cpp index 8a1419faa44..174d2e4dacd 100644 --- a/indra/newview/llfloaterpreference.cpp +++ b/indra/newview/llfloaterpreference.cpp @@ -42,9 +42,11 @@ #include "llcolorswatch.h" #include "llcombobox.h" #include "llcommandhandler.h" +#include "lldiriterator.h" #include "lldirpicker.h" #include "lleventtimer.h" #include "llfeaturemanager.h" +#include "llfilepicker.h" #include "llfocusmgr.h" //#include "llfirstuse.h" #include "llfloaterreg.h" @@ -85,6 +87,7 @@ #include "llfontgl.h" #include "llrect.h" #include "llstring.h" +#include "alunzip.h" // project includes @@ -96,6 +99,7 @@ #include "llstartup.h" #include "lltextbox.h" #include "llui.h" +#include "llversioninfo.h" #include "llviewerobjectlist.h" #include "llvoavatar.h" #include "llvovolume.h" @@ -121,6 +125,9 @@ #include "llfeaturemanager.h" #include "llviewertexturelist.h" +#include <json/json.h> +#include <utility> + #include "llsearchableui.h" const F32 BANDWIDTH_UPDATER_TIMEOUT = 0.5f; @@ -163,6 +170,8 @@ struct LabelTable : public LLInitParam::Block<LabelTable> {} }; +const std::string DEFAULT_SKIN = "default"; + class LLVoiceSetKeyDialog : public LLModalDialog { public: @@ -181,6 +190,24 @@ class LLVoiceSetKeyDialog : public LLModalDialog LLFloaterPreference* mParent; }; +typedef enum e_skin_type +{ + SYSTEM_SKIN, + USER_SKIN +} ESkinType; + +typedef struct skin_t +{ + std::string mName = "Unknown"; + std::string mAuthor = "Unknown"; + std::string mUrl = "Unknown"; + LLDate mDate = LLDate(0.0); + std::string mCompatVer = "Unknown"; + std::string mNotes = LLStringUtil::null; + ESkinType mType = USER_SKIN; + +} skin_t; + LLVoiceSetKeyDialog::LLVoiceSetKeyDialog(const LLSD& key) : LLModalDialog(key), mParent(NULL) @@ -399,8 +426,6 @@ LLFloaterPreference::LLFloaterPreference(const LLSD& key) mCommitCallbackRegistrar.add("Pref.WebClearCache", boost::bind(&LLFloaterPreference::onClickBrowserClearCache, this)); mCommitCallbackRegistrar.add("Pref.SetCache", boost::bind(&LLFloaterPreference::onClickSetCache, this)); mCommitCallbackRegistrar.add("Pref.ResetCache", boost::bind(&LLFloaterPreference::onClickResetCache, this)); - mCommitCallbackRegistrar.add("Pref.ClickSkin", boost::bind(&LLFloaterPreference::onClickSkin, this,_1, _2)); - mCommitCallbackRegistrar.add("Pref.SelectSkin", boost::bind(&LLFloaterPreference::onSelectSkin, this)); mCommitCallbackRegistrar.add("Pref.VoiceSetKey", boost::bind(&LLFloaterPreference::onClickSetKey, this)); mCommitCallbackRegistrar.add("Pref.VoiceSetMiddleMouse", boost::bind(&LLFloaterPreference::onClickSetMiddleMouse, this)); mCommitCallbackRegistrar.add("Pref.SetSounds", boost::bind(&LLFloaterPreference::onClickSetSounds, this)); @@ -442,6 +467,11 @@ LLFloaterPreference::LLFloaterPreference(const LLSD& key) mCommitCallbackRegistrar.add("Pref.ClearLog", boost::bind(&LLConversationLog::onClearLog, &LLConversationLog::instance())); mCommitCallbackRegistrar.add("Pref.DeleteTranscripts", boost::bind(&LLFloaterPreference::onDeleteTranscripts, this)); mCommitCallbackRegistrar.add("UpdateFilter", boost::bind(&LLFloaterPreference::onUpdateFilterTerm, this, false)); // <FS:ND/> Hook up for filtering + + mCommitCallbackRegistrar.add("Pref.AddSkin", boost::bind(&LLFloaterPreference::onAddSkin, this)); + mCommitCallbackRegistrar.add("Pref.RemoveSkin", boost::bind(&LLFloaterPreference::onRemoveSkin, this)); + mCommitCallbackRegistrar.add("Pref.ApplySkin", boost::bind(&LLFloaterPreference::onApplySkin, this)); + mCommitCallbackRegistrar.add("Pref.SelectSkin", boost::bind(&LLFloaterPreference::onSelectSkin, this, _2)); } void LLFloaterPreference::processProperties( void* pData, EAvatarProcessorType type ) @@ -551,6 +581,9 @@ BOOL LLFloaterPreference::postBuild() changed(); LLLogChat::getInstance()->setSaveHistorySignal(boost::bind(&LLFloaterPreference::onLogChatHistorySaved, this)); + + loadUserSkins(); + LLSliderCtrl* fov_slider = getChild<LLSliderCtrl>("camera_fov"); fov_slider->setMinValue(LLViewerCamera::getInstance()->getMinView()); @@ -603,6 +636,266 @@ void LLFloaterPreference::onDoNotDisturbResponseChanged() gSavedPerAccountSettings.setBOOL("DoNotDisturbResponseChanged", response_changed_flag ); } + +//////////////////////////////////////////////////// +// Skins panel + +skin_t manifestFromJson(const std::string& filename, const ESkinType type) +{ + skin_t skin; + Json::Reader reader; + Json::Value root; + + llifstream in; + in.open(filename); + if (in.is_open()) + { + if (reader.parse(in, root, false)) + { + skin.mName = root.get("name", "Unknown").asString(); + skin.mAuthor = root.get("author", "Unknown").asString(); + skin.mUrl = root.get("url", "Unknown").asString(); + skin.mCompatVer = root.get("compatibility", "Unknown").asString(); + skin.mDate = LLDate(root.get("date", "1983-04-20T00:00:00+00:00").asString()); + skin.mNotes = root.get("notes", "").asString(); + // If it's a system skin, the compatability version is always the current build + if (type == SYSTEM_SKIN) + { + skin.mCompatVer = LLVersionInfo::getShortVersion(); + } + } + else + { + LL_WARNS() << "Failed to parse " << filename << ": " << reader.getFormatedErrorMessages() << LL_ENDL; + } + in.close(); + } + skin.mType = type; + return skin; +} + +void LLFloaterPreference::loadUserSkins() +{ + mUserSkins.clear(); + LLDirIterator sysiter(gDirUtilp->getSkinBaseDir(), "*"); + bool found = true; + while (found) + { + std::string dir; + if ((found = sysiter.next(dir))) + { + const std::string& fullpath = gDirUtilp->add(gDirUtilp->getSkinBaseDir(), dir); + if (!LLFile::isdir(fullpath)) continue; // only directories! + + const std::string& manifestpath = gDirUtilp->add(fullpath, "manifest.json"); + skin_t skin = manifestFromJson(manifestpath, SYSTEM_SKIN); + + mUserSkins.emplace(dir, skin); + } + } + + const std::string userskindir = gDirUtilp->add(gDirUtilp->getOSUserAppDir(), "skins"); + if (LLFile::isdir(userskindir)) + { + LLDirIterator iter(userskindir, "*"); + found = true; + while (found) + { + std::string dir; + if ((found = iter.next(dir))) + { + const std::string& fullpath = gDirUtilp->add(userskindir, dir); + if (!LLFile::isdir(fullpath)) continue; // only directories! + + const std::string& manifestpath = gDirUtilp->add(fullpath, "manifest.json"); + skin_t skin = manifestFromJson(manifestpath, USER_SKIN); + + mUserSkins.emplace(dir, skin); + } + } + } + reloadSkinList(); +} + +void LLFloaterPreference::reloadSkinList() +{ + LLScrollListCtrl* skin_list = getChild<LLScrollListCtrl>("skin_list"); + const std::string current_skin = gSavedSettings.getString("SkinCurrent"); + + skin_list->clearRows(); + + // User Downloaded Skins + for (const auto& skin : mUserSkins) + { + LLSD row; + row["id"] = skin.first; + row["columns"][0]["value"] = skin.second.mName == "Unknown" ? skin.first : skin.second.mName; + row["columns"][0]["font"]["style"] = current_skin == skin.first ? "BOLD" : "NORMAL"; + skin_list->addElement(row); + } + skin_list->setSelectedByValue(current_skin, TRUE); + onSelectSkin(skin_list->getSelectedValue()); +} + +void LLFloaterPreference::onAddSkin() +{ + LLFilePicker& filepicker = LLFilePicker::instance(); + if (filepicker.getOpenFile(LLFilePicker::FFLOAD_ZIP)) + { + const std::string& package = filepicker.getFirstFile(); + auto zip = std::make_unique<ALUnZip>(package); + if (zip->isValid()) + { + size_t buf_size = zip->getSizeFile("manifest.json"); + if (buf_size) + { + buf_size++; + buf_size *= sizeof(char); + auto buf = std::make_unique<char[]>(buf_size); + zip->extractFile("manifest.json", buf.get(), buf_size); + buf[buf_size - 1] = '\0'; // force. + std::stringstream ss; + ss << std::string(const_cast<const char*>(buf.get()), buf_size); + buf.reset(); + + Json::Reader reader; + Json::Value root; + std::string errors; + if (reader.parse(ss, root, false)) + { + const std::string& name = root.get("name", "Unknown").asString(); + std::string pathname = gDirUtilp->add(gDirUtilp->getOSUserAppDir(), "skins"); + if (!gDirUtilp->fileExists(pathname)) + { + LLFile::mkdir(pathname); + } + pathname = gDirUtilp->add(pathname, name); + if (!LLFile::isdir(pathname) && (LLFile::mkdir(pathname) != 0)) + { + LLNotificationsUtil::add("AddSkinUnpackFailed"); + } + else if (!zip->extract(pathname)) + { + LLNotificationsUtil::add("AddSkinUnpackFailed"); + } + else + { + loadUserSkins(); + LLNotificationsUtil::add("AddSkinSuccess", LLSD().with("PACKAGE", name)); + } + } + else + { + LLNotificationsUtil::add("AddSkinCantParseManifest", LLSD().with("PACKAGE", package)); + } + } + else + { + LLNotificationsUtil::add("AddSkinNoManifest", LLSD().with("PACKAGE", package)); + } + } + } +} + +void LLFloaterPreference::onRemoveSkin() +{ + LLScrollListCtrl* skin_list = findChild<LLScrollListCtrl>("skin_list"); + if (skin_list) + { + LLSD args; + args["SKIN"] = skin_list->getSelectedValue().asString(); + LLNotificationsUtil::add("ConfirmRemoveSkin", args, args, + boost::bind(&LLFloaterPreference::callbackRemoveSkin, this, _1, _2)); + } +} + +void LLFloaterPreference::callbackRemoveSkin(const LLSD& notification, const LLSD& response) +{ + S32 option = LLNotificationsUtil::getSelectedOption(notification, response); + if (option == 0) // YES + { + const std::string& skin = notification["payload"]["SKIN"].asString(); + std::string dir = gDirUtilp->add(gDirUtilp->getOSUserAppDir(), "skins"); + dir = gDirUtilp->add(dir, skin); + if (gDirUtilp->deleteDirAndContents(dir) > 0) + { + skinmap_t::iterator iter = mUserSkins.find(skin); + if (iter != mUserSkins.end()) + mUserSkins.erase(iter); + // If we just deleted the current skin, reset to default. It might not even be a good + // idea to allow this, but we'll see! + if (gSavedSettings.getString("SkinCurrent") == skin) + { + gSavedSettings.setString("SkinCurrent", DEFAULT_SKIN); + } + LLNotificationsUtil::add("RemoveSkinSuccess", LLSD().with("SKIN", skin)); + } + else + { + LLNotificationsUtil::add("RemoveSkinFailure", LLSD().with("SKIN", skin)); + } + reloadSkinList(); + } +} + +void LLFloaterPreference::callbackApplySkin(const LLSD& notification, const LLSD& response) +{ + S32 option = LLNotificationsUtil::getSelectedOption(notification, response); + switch (option) + { + case 0: // Yes + gSavedSettings.setBOOL("ResetUserColorsOnLogout", TRUE); + break; + case 1: // No + gSavedSettings.setBOOL("ResetUserColorsOnLogout", FALSE); + break; + case 2: // Cancel + gSavedSettings.setString("SkinCurrent", sSkin); + reloadSkinList(); + break; + default: + LL_WARNS() << "Unhandled option! How could this be?" << LL_ENDL; + break; + } +} + +void LLFloaterPreference::onApplySkin() +{ + LLScrollListCtrl* skin_list = findChild<LLScrollListCtrl>("skin_list"); + if (skin_list) + { + gSavedSettings.setString("SkinCurrent", skin_list->getSelectedValue().asString()); + reloadSkinList(); + } + if (sSkin != gSavedSettings.getString("SkinCurrent")) + { + LLNotificationsUtil::add("ChangeSkin", LLSD(), LLSD(), + boost::bind(&LLFloaterPreference::callbackApplySkin, this, _1, _2)); + } +} + +void LLFloaterPreference::onSelectSkin(const LLSD& data) +{ + bool userskin = false; + skinmap_t::iterator iter = mUserSkins.find(data.asString()); + if (iter != mUserSkins.end()) + { + refreshSkinInfo(iter->second); + userskin = (iter->second.mType == USER_SKIN); + } + getChild<LLUICtrl>("remove_skin")->setEnabled(userskin); +} + +void LLFloaterPreference::refreshSkinInfo(const skin_t& skin) +{ + getChild<LLTextBase>("skin_name")->setText(skin.mName); + getChild<LLTextBase>("skin_author")->setText(skin.mAuthor); + getChild<LLTextBase>("skin_homepage")->setText(skin.mUrl); + getChild<LLTextBase>("skin_date")->setText(skin.mDate.toHTTPDateString("%A, %d %b %Y")); + getChild<LLTextBase>("skin_compatibility")->setText(skin.mCompatVer); + getChild<LLTextBase>("skin_notes")->setText(skin.mNotes); +} + LLFloaterPreference::~LLFloaterPreference() { LLConversationLog::instance().removeObserver(this); @@ -640,8 +933,7 @@ void LLFloaterPreference::apply() LLTabContainer* tabcontainer = getChild<LLTabContainer>("pref core"); if (sSkin != gSavedSettings.getString("SkinCurrent")) { - LLNotificationsUtil::add("ChangeSkin"); - refreshSkin(this); + sSkin = gSavedSettings.getString("SkinCurrent"); } // Call apply() on all panels that derive from LLPanelPreference for (child_list_t::const_iterator iter = tabcontainer->getChildList()->begin(); @@ -1220,25 +1512,6 @@ void LLFloaterPreference::onClickResetCache() gSavedSettings.setString("CacheLocationTopFolder", top_folder); } -void LLFloaterPreference::onClickSkin(LLUICtrl* ctrl, const LLSD& userdata) -{ - gSavedSettings.setString("SkinCurrent", userdata.asString()); - ctrl->setValue(userdata.asString()); -} - -void LLFloaterPreference::onSelectSkin() -{ - std::string skin_selection = getChild<LLRadioGroup>("skin_selection")->getValue().asString(); - gSavedSettings.setString("SkinCurrent", skin_selection); -} - -void LLFloaterPreference::refreshSkin(void* data) -{ - LLPanel*self = (LLPanel*)data; - sSkin = gSavedSettings.getString("SkinCurrent"); - self->getChild<LLRadioGroup>("skin_selection", true)->setValue(sSkin); -} - void LLFloaterPreference::buildPopupLists() { LLScrollListCtrl& disabled_popups = @@ -2409,21 +2682,6 @@ BOOL LLPanelPreference::postBuild() getChildView("voice_unavailable")->setVisible( voice_disabled); getChildView("enable_voice_check")->setVisible( !voice_disabled); } - - //////////////////////PanelSkins /////////////////// - - if (hasChild("skin_selection", TRUE)) - { - LLFloaterPreference::refreshSkin(this); - - // if skin is set to a skin that no longer exists (silver) set back to default - if (getChild<LLRadioGroup>("skin_selection")->getSelectedIndex() < 0) - { - gSavedSettings.setString("SkinCurrent", "default"); - LLFloaterPreference::refreshSkin(this); - } - - } //////////////////////PanelPrivacy /////////////////// if (hasChild("media_enabled", TRUE)) diff --git a/indra/newview/llfloaterpreference.h b/indra/newview/llfloaterpreference.h index a0f43bd8848..2dbea689813 100644 --- a/indra/newview/llfloaterpreference.h +++ b/indra/newview/llfloaterpreference.h @@ -47,6 +47,7 @@ class LLScrollListCtrl; class LLSliderCtrl; class LLSD; class LLTextBox; +struct skin_t; namespace ll { @@ -144,8 +145,6 @@ class LLFloaterPreference : public LLFloater, public LLAvatarPropertiesObserver, void onClickSetCache(); void changeCachePath(const std::vector<std::string>& filenames, std::string proposed_name); void onClickResetCache(); - void onClickSkin(LLUICtrl* ctrl,const LLSD& userdata); - void onSelectSkin(); void onClickSetKey(); void setKey(KEY key); void setMouse(LLMouseHandler::EClickType click); @@ -201,6 +200,17 @@ class LLFloaterPreference : public LLFloater, public LLAvatarPropertiesObserver, void updateMaxComplexity(); static bool loadFromFilename(const std::string& filename, std::map<std::string, std::string> &label_map); + + void loadUserSkins(); + void reloadSkinList(); + void onAddSkin(); + void onRemoveSkin(); + void callbackRemoveSkin(const LLSD& notification, const LLSD& response); + void onApplySkin(); + void callbackApplySkin(const LLSD& notification, const LLSD& response); + void onSelectSkin(const LLSD& data); + void refreshSkinInfo(const skin_t& skin); + static std::string sSkin; notifications_map mNotificationOptions; bool mClickActionDirty; ///< Set to true when the click/double-click options get changed by user. @@ -215,6 +225,10 @@ class LLFloaterPreference : public LLFloater, public LLAvatarPropertiesObserver, LLAvatarData mAvatarProperties; std::string mSavedGraphicsPreset; + + typedef std::map<std::string, skin_t> skinmap_t; + skinmap_t mUserSkins; + LOG_CLASS(LLFloaterPreference); LLSearchEditor *mFilterEdit; diff --git a/indra/newview/skins/default/manifest.json b/indra/newview/skins/default/manifest.json new file mode 100644 index 00000000000..6fc90b73408 --- /dev/null +++ b/indra/newview/skins/default/manifest.json @@ -0,0 +1,6 @@ +{ + "name": "Second Life", + "author": "Linden Lab", + "url": "http://www.secondlife.com/", + "notes": "The classic skin from the Second Life Viewer" +} diff --git a/indra/newview/skins/default/xui/en/floater_preferences.xml b/indra/newview/skins/default/xui/en/floater_preferences.xml index 0e62d50072e..0608a822152 100644 --- a/indra/newview/skins/default/xui/en/floater_preferences.xml +++ b/indra/newview/skins/default/xui/en/floater_preferences.xml @@ -169,6 +169,11 @@ https://accounts.secondlife.com/change_email/ layout="topleft" help_topic="preferences_uploads_tab" name="uploads" /> + <panel + filename="panel_preferences_skins.xml" + label="Skinning" + layout="topleft" + name="skins" /> </tab_container> </floater> diff --git a/indra/newview/skins/default/xui/en/notifications.xml b/indra/newview/skins/default/xui/en/notifications.xml index 6dd9b8ec7c1..ba62372f7db 100644 --- a/indra/newview/skins/default/xui/en/notifications.xml +++ b/indra/newview/skins/default/xui/en/notifications.xml @@ -11684,4 +11684,60 @@ Unable to load the track from [TRACK1] into [TRACK2]. <tag>fail</tag> </notification> + <!-- ALCHEMY BELOW THIS LINE --> + <notification + icon="alertmodal.tga" + name="ConfirmRemoveSkin" + type="alertmodal"> + <tag>confirm</tag> + Are you sure you want to remove [SKIN]? This will erase it permenantly from your hard disk. + <usetemplate + ignoretext="Confirm removing skins" + name="okcancelignore" + notext="Cancel" + yestext="OK"/> + </notification> + <notification + icon="alertmodal.tga" + name="RemoveSkinSuccess" + type="alertmodal"> + <tag>fail</tag> + Removed [SKIN] successfully. + </notification> + <notification + icon="alertmodal.tga" + name="RemoveSkinFailure" + type="alertmodal"> + <tag>fail</tag> + Failed to remove [SKIN]. + </notification> + <notification + icon="alertmodal.tga" + name="AddSkinNoManifest" + type="alertmodal"> + <tag>fail</tag> + [PACKAGE] doesn't contain a manifest or is unreadable. + </notification> + <notification + icon="alertmodal.tga" + name="AddSkinCantParseManifest" + type="alertmodal"> + <tag>fail</tag> + [PACKAGE] has an invalid manifest. + </notification> + <notification + icon="alertmodal.tga" + name="AddSkinUnpackFailed" + type="alertmodal"> + <tag>fail</tag> + Failed to unpack the skin package. See the log for more details. + </notification> + <notification + icon="alertmodal.tga" + name="AddSkinSuccess" + type="alertmodal"> + <tag>fail</tag> + [PACKAGE] was added successfully. + </notification> + </notifications> diff --git a/indra/newview/skins/default/xui/en/panel_preferences_skins.xml b/indra/newview/skins/default/xui/en/panel_preferences_skins.xml new file mode 100644 index 00000000000..6bee06a9abc --- /dev/null +++ b/indra/newview/skins/default/xui/en/panel_preferences_skins.xml @@ -0,0 +1,203 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<panel + border="true" + follows="left|top|right|bottom" + height="408" + label="Skinning" + layout="topleft" + left="102" + name="skins" + top="1" + width="517"> + <text + type="string" + length="1" + follows="left|top" + height="12" + layout="topleft" + left="30" + name="select_skin_text" + top="12" + width="180"> + Select a skin: + </text> + <!-- Chooser --> + <scroll_list + height="225" + follows="left|top" + layout="topleft" + left="40" + name="skin_list" + draw_heading="false" + top_pad="8" + width="170"> + <scroll_list.commit_callback + function="Pref.SelectSkin" /> + </scroll_list> + <button + enabled="true" + follows="left|top" + height="19" + label="Apply" + layout="topleft" + left="40" + name="apply_skin" + tool_tip="Changes will take effect after you restart [APP_NAME]" + top_pad="4" + width="75"> + <button.commit_callback + function="Pref.ApplySkin" /> + </button> + <button + enabled="true" + follows="left|top" + height="19" + image_disabled="AddItem_Disabled" + image_selected="AddItem_Press" + image_unselected="AddItem_Off" + layout="topleft" + left_pad="52" + name="add_skin" + top_delta="0" + tool_tip="Add a new skin..." + width="19"> + <button.commit_callback + function="Pref.AddSkin" /> + </button> + <button + enabled="false" + follows="left|top" + height="19" + image_disabled="TrashItem_Disabled" + image_selected="TrashItem_Press" + image_unselected="TrashItem_Off" + layout="topleft" + left_pad="4" + name="remove_skin" + top_delta="0" + tool_tip="Remove selected skin..." + width="19"> + <button.commit_callback + function="Pref.RemoveSkin" /> + </button> + <!-- Info/Preview --> + <text + type="string" + length="1" + follows="left|top" + height="20" + layout="topleft" + font="SansSerifLarge" + left="215" + name="skin_name" + top="10" + right="-10"> + Boring skin! + </text> + <text + type="string" + length="1" + follows="left|top" + height="14" + layout="topleft" + name="skin_author_label" + top_pad="2" + width="90" + value="Author:" /> + <text + type="string" + length="1" + follows="left|top" + height="14" + layout="topleft" + name="skin_homepage_label" + top_pad="2" + width="90" + value="Homepage:" /> + <text + type="string" + length="1" + follows="left|top" + height="14" + layout="topleft" + name="skin_date_label" + top_pad="2" + width="90" + value="Date:" /> + <text + type="string" + length="1" + follows="left|top" + height="14" + layout="topleft" + name="skin_compatibility_label" + top_pad="2" + width="90" + value="Compatiblity:" /> + <text + type="string" + length="1" + follows="left|top" + height="14" + layout="topleft" + name="skin_author" + top_delta="-48" + left_pad="1" + width="200"> +Nobody + </text> + <text + type="string" + length="1" + follows="left|top" + height="14" + layout="topleft" + name="skin_homepage" + top_pad="2" + width="200"> +http://www.example.com/ + </text> + <text + type="string" + length="1" + follows="left|top" + height="14" + layout="topleft" + name="skin_date" + top_pad="2" + width="200"> +Jan 1, 2001 + </text> + <text + type="string" + length="1" + follows="left|top" + height="14" + layout="topleft" + name="skin_compatibility" + top_pad="2" + width="200"> +0.0.0 + </text> + <text + type="string" + length="1" + follows="left|top" + height="14" + layout="topleft" + name="skin_notes_label" + top_pad="2" + left="215" + width="90" + value="Notes:" /> + <text + type="string" + length="1" + follows="left|top" + height="70" + layout="topleft" + name="skin_notes" + top_pad="2" + word_wrap="true" + width="291" /> +</panel> diff --git a/indra/newview/skins/default/xui/en/strings.xml b/indra/newview/skins/default/xui/en/strings.xml index e95d4701bbc..e3163d6c857 100644 --- a/indra/newview/skins/default/xui/en/strings.xml +++ b/indra/newview/skins/default/xui/en/strings.xml @@ -573,6 +573,7 @@ http://secondlife.com/support for help fixing this problem. <string name="choose_the_directory">Choose Directory</string> <string name="script_files">Scripts</string> <string name="dictionary_files">Dictionaries</string> + <string name="zip_files">Zip files</string> <!-- LSL Usage Hover Tips --> <!-- NOTE: For now these are set as translate="false", until DEV-40761 is implemented (to internationalize the rest of tooltips in the same window). diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py index c74dbb9e302..2d1fea60e51 100755 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -154,6 +154,7 @@ def construct(self): self.path("*/xui/*/*.xml") self.path("*/xui/*/widgets/*.xml") self.path("*/*.xml") + self.path("*/*.json") # Local HTML files (e.g. loading screen) # The claim is that we never use local html files any -- GitLab