/** * @file llkeyconflict.cpp * @brief * * $LicenseInfo:firstyear=2019&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2019, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License only. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ /* * App-wide preferences. Note that these are not per-user, * because we need to load many preferences before we have * a login name. */ #include "llviewerprecompiledheaders.h" #include "llkeyconflict.h" #include "llinitparam.h" #include "llkeyboard.h" #include "llviewercontrol.h" #include "llviewerinput.h" #include "llviewermenu.h" #include "llxuiparser.h" static const std::string saved_settings_key_controls[] = { "placeholder" }; // add settings from gSavedSettings here static const std::string filename_default = "key_bindings.xml"; static const std::string filename_temporary = "key_bindings_tmp.xml"; // used to apply uncommited changes on the go. // LLKeyboard::stringFromMask is meant for UI and is OS dependent, // so this class uses it's own version std::string string_from_mask(MASK mask) { std::string res; if ((mask & MASK_CONTROL) != 0) { res = "CTL"; } if ((mask & MASK_ALT) != 0) { if (!res.empty()) res += "_"; res += "ALT"; } if ((mask & MASK_SHIFT) != 0) { if (!res.empty()) res += "_"; res += "SHIFT"; } if (mask == MASK_NONE) { res = "NONE"; } return res; } std::string string_from_mouse(EMouseClickType click) { std::string res; switch (click) { case CLICK_LEFT: res = "LMB"; break; case CLICK_MIDDLE: res = "MMB"; break; case CLICK_RIGHT: res = "RMB"; break; case CLICK_BUTTON4: res = "MB4"; break; case CLICK_BUTTON5: res = "MB5"; break; case CLICK_DOUBLELEFT: res = "Double LMB"; break; default: break; } return res; } EMouseClickType mouse_from_string(const std::string& input) { if (input == "LMB") { return CLICK_LEFT; } if (input == "MMB") { return CLICK_MIDDLE; } if (input == "RMB") { return CLICK_RIGHT; } if (input == "MB4") { return CLICK_BUTTON4; } if (input == "MB5") { return CLICK_BUTTON5; } if (input == "Double LMB") { return CLICK_DOUBLELEFT; } return CLICK_NONE; } // LLKeyConflictHandler S32 LLKeyConflictHandler::sTemporaryFileUseCount = 0; LLKeyConflictHandler::LLKeyConflictHandler() : mHasUnsavedChanges(false), mUsesTemporaryFile(false), mLoadMode(MODE_COUNT) { } LLKeyConflictHandler::LLKeyConflictHandler(ESourceMode mode) : mHasUnsavedChanges(false), mUsesTemporaryFile(false), mLoadMode(mode) { loadFromSettings(mode); } LLKeyConflictHandler::~LLKeyConflictHandler() { clearUnsavedChanges(); // Note: does not reset bindings if temporary file was used } bool LLKeyConflictHandler::canHandleControl(const std::string &control_name, EMouseClickType mouse_ind, KEY key, MASK mask) { return mControlsMap[control_name].canHandle(mouse_ind, key, mask); } bool LLKeyConflictHandler::canHandleKey(const std::string &control_name, KEY key, MASK mask) { return canHandleControl(control_name, CLICK_NONE, key, mask); } bool LLKeyConflictHandler::canHandleMouse(const std::string &control_name, EMouseClickType mouse_ind, MASK mask) { return canHandleControl(control_name, mouse_ind, KEY_NONE, mask); } bool LLKeyConflictHandler::canHandleMouse(const std::string &control_name, S32 mouse_ind, MASK mask) { return canHandleControl(control_name, (EMouseClickType)mouse_ind, KEY_NONE, mask); } bool LLKeyConflictHandler::canAssignControl(const std::string &control_name) { control_map_t::iterator iter = mControlsMap.find(control_name); if (iter != mControlsMap.end()) { return iter->second.mAssignable; } // If we don't know this control means it wasn't assigned by user yet and thus is editable return true; } // static bool LLKeyConflictHandler::isReservedByMenu(const KEY &key, const MASK &mask) { if (key == KEY_NONE) { return false; } return (gMenuBarView && gMenuBarView->hasAccelerator(key, mask)) || (gLoginMenuBarView && gLoginMenuBarView->hasAccelerator(key, mask)); } // static bool LLKeyConflictHandler::isReservedByMenu(const LLKeyData &data) { if (data.mMouse != CLICK_NONE || data.mKey == KEY_NONE) { return false; } return (gMenuBarView && gMenuBarView->hasAccelerator(data.mKey, data.mMask)) || (gLoginMenuBarView && gLoginMenuBarView->hasAccelerator(data.mKey, data.mMask)); } bool LLKeyConflictHandler::registerControl(const std::string &control_name, U32 index, EMouseClickType mouse, KEY key, MASK mask, bool ignore_mask) { if (control_name.empty()) { return false; } LLKeyConflict &type_data = mControlsMap[control_name]; if (!type_data.mAssignable) { LL_ERRS() << "Error in code, user or system should not be able to change certain controls" << LL_ENDL; } LLKeyData data(mouse, key, mask, ignore_mask); if (type_data.mKeyBind.getKeyData(index) == data) { return true; } if (isReservedByMenu(data)) { return false; } if (removeConflicts(data, type_data.mConflictMask)) { type_data.mKeyBind.replaceKeyData(data, index); mHasUnsavedChanges = true; return true; } // control already in use/blocked return false; } LLKeyData LLKeyConflictHandler::getControl(const std::string &control_name, U32 index) { if (control_name.empty()) { return LLKeyData(); } return mControlsMap[control_name].getKeyData(index); } // static std::string LLKeyConflictHandler::getStringFromKeyData(const LLKeyData& keydata) { std::string result; if (keydata.mMask != MASK_NONE && keydata.mKey != KEY_NONE) { result = LLKeyboard::stringFromAccelerator(keydata.mMask, keydata.mKey); } else if (keydata.mKey != KEY_NONE) { result = LLKeyboard::stringFromKey(keydata.mKey); } else if (keydata.mMask != MASK_NONE) { result = LLKeyboard::stringFromAccelerator(keydata.mMask); } result += string_from_mouse(keydata.mMouse); return result; } std::string LLKeyConflictHandler::getControlString(const std::string &control_name, U32 index) { if (control_name.empty()) { return ""; } return getStringFromKeyData(mControlsMap[control_name].getKeyData(index)); } void LLKeyConflictHandler::loadFromControlSettings(const std::string &name) { LLControlVariablePtr var = gSavedSettings.getControl(name); if (var) { LLKeyBind bind(var->getValue()); LLKeyConflict key(bind, true, 0); mControlsMap[name] = key; } } void LLKeyConflictHandler::loadFromSettings(const LLViewerInput::KeyMode& keymode, control_map_t *destination) { for (LLInitParam::ParamIterator::const_iterator it = keymode.bindings.begin(), end_it = keymode.bindings.end(); it != end_it; ++it) { KEY key; MASK mask; EMouseClickType mouse = it->mouse.isProvided() ? mouse_from_string(it->mouse) : CLICK_NONE; if (it->key.getValue().empty()) { key = KEY_NONE; } else { LLKeyboard::keyFromString(it->key, &key); } LLKeyboard::maskFromString(it->mask, &mask); // Note: it->command is also the name of UI element, howhever xml we are loading from // might not know all the commands, so UI will have to know what to fill by its own LLKeyConflict &type_data = (*destination)[it->command]; type_data.mAssignable = true; type_data.mKeyBind.addKeyData(mouse, key, mask, true); } } bool LLKeyConflictHandler::loadFromSettings(const ESourceMode &load_mode, const std::string &filename, control_map_t *destination) { if (filename.empty()) { return false; } bool res = false; LLViewerInput::Keys keys; LLSimpleXUIParser parser; if (parser.readXUI(filename, keys) && keys.validateBlock()) { switch (load_mode) { case MODE_FIRST_PERSON: if (keys.first_person.isProvided()) { loadFromSettings(keys.first_person, destination); res = true; } break; case MODE_THIRD_PERSON: if (keys.third_person.isProvided()) { loadFromSettings(keys.third_person, destination); res = true; } break; case MODE_EDIT_AVATAR: if (keys.edit_avatar.isProvided()) { loadFromSettings(keys.edit_avatar, destination); res = true; } break; case MODE_SITTING: if (keys.sitting.isProvided()) { loadFromSettings(keys.sitting, destination); res = true; } break; default: LL_ERRS() << "Not implememted mode " << load_mode << LL_ENDL; break; } } return res; } void LLKeyConflictHandler::loadFromSettings(ESourceMode load_mode) { mControlsMap.clear(); mDefaultsMap.clear(); // E.X. In case we need placeholder keys for conflict resolution. generatePlaceholders(load_mode); if (load_mode == MODE_SAVED_SETTINGS) { // load settings clss knows about, but it also possible to load settings by name separately const S32 size = std::extent::value; for (U32 i = 0; i < size; i++) { loadFromControlSettings(saved_settings_key_controls[i]); } } else { // load defaults std::string filename = gDirUtilp->getExpandedFilename(LL_PATH_APP_SETTINGS, filename_default); if (!loadFromSettings(load_mode, filename, &mDefaultsMap)) { LL_WARNS() << "Failed to load default settings, aborting" << LL_ENDL; return; } // load user's filename = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, filename_default); if (!gDirUtilp->fileExists(filename) || !loadFromSettings(load_mode, filename, &mControlsMap)) { // mind placeholders mControlsMap.insert(mDefaultsMap.begin(), mDefaultsMap.end()); } } mLoadMode = load_mode; } void LLKeyConflictHandler::saveToSettings(bool temporary) { if (mControlsMap.empty()) { return; } if (mLoadMode == MODE_SAVED_SETTINGS) { // Does not support 'temporary', preferences handle that themself // so in case of saved settings we just do not clear mHasUnsavedChanges control_map_t::iterator iter = mControlsMap.begin(); control_map_t::iterator end = mControlsMap.end(); for (; iter != end; ++iter) { if (iter->first.empty()) { continue; } LLKeyConflict &key = iter->second; key.mKeyBind.trimEmpty(); if (!key.mAssignable) { continue; } if (gSavedSettings.controlExists(iter->first)) { gSavedSettings.setLLSD(iter->first, key.mKeyBind.asLLSD()); } else if (!key.mKeyBind.empty()) { // Note: this is currently not in use, might be better for load mechanics to ask for and retain control group // otherwise settings loaded from other control groups will end in this one LL_INFOS() << "Creating new keybinding " << iter->first << LL_ENDL; gSavedSettings.declareLLSD(iter->first, key.mKeyBind.asLLSD(), "comment", LLControlVariable::PERSIST_ALWAYS); } } } else { // Determine what file to load and load full copy of that file std::string filename; if (temporary) { filename = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, filename_temporary); if (!gDirUtilp->fileExists(filename)) { filename.clear(); } } if (filename.empty()) { filename = gDirUtilp->findFile(filename_default, gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, ""), gDirUtilp->getExpandedFilename(LL_PATH_APP_SETTINGS, "")); } LLViewerInput::Keys keys; LLSimpleXUIParser parser; if (parser.readXUI(filename, keys) && keys.validateBlock()) { // replace category we edited // mode is a HACK to correctly reset bindings without reparsing whole file and avoid doing // own param container (which will face issues with inasseesible members of LLInitParam) LLViewerInput::KeyMode mode; LLViewerInput::KeyBinding binding; control_map_t::iterator iter = mControlsMap.begin(); control_map_t::iterator end = mControlsMap.end(); for (; iter != end; ++iter) { // By default xml have (had) up to 6 elements per function // eventually it will be cleaned up and UI will only shows 3 per function, // so make sure to cleanup. // Also this helps in keeping file small. iter->second.mKeyBind.trimEmpty(); U32 size = iter->second.mKeyBind.getDataCount(); for (U32 i = 0; i < size; ++i) { if (iter->first.empty()) { continue; } LLKeyConflict &key = iter->second; key.mKeyBind.trimEmpty(); if (key.mKeyBind.empty() || !key.mAssignable) { continue; } LLKeyData data = key.mKeyBind.getKeyData(i); // Still write empty LLKeyData to make sure we will maintain UI position if (data.mKey == KEY_NONE) { // Might be better idea to be consistent and use NONE. LLViewerInput can work with both cases binding.key = ""; } else { // Note: this is UI string, we might want to hardcode our own for 'fixed' use in keys.xml binding.key = LLKeyboard::stringFromKey(data.mKey); } binding.mask = string_from_mask(data.mMask); if (data.mMouse == CLICK_NONE) { binding.mouse.setProvided(false); } else { // set() because 'optional', for compatibility purposes // just copy old keys.xml and rename to key_bindings.xml, it should work binding.mouse.set(string_from_mouse(data.mMouse), true); } binding.command = iter->first; mode.bindings.add(binding); } } switch (mLoadMode) { case MODE_FIRST_PERSON: if (keys.first_person.isProvided()) { keys.first_person.bindings.set(mode.bindings, true); } break; case MODE_THIRD_PERSON: if (keys.third_person.isProvided()) { keys.third_person.bindings.set(mode.bindings, true); } break; case MODE_EDIT_AVATAR: if (keys.edit_avatar.isProvided()) { keys.edit_avatar.bindings.set(mode.bindings, true); } break; case MODE_SITTING: if (keys.sitting.isProvided()) { keys.sitting.bindings.set(mode.bindings, true); } break; default: LL_ERRS() << "Not implememted mode " << mLoadMode << LL_ENDL; break; } if (temporary) { // write to temporary xml and use it for gViewerInput filename = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, filename_temporary); if (!mUsesTemporaryFile) { mUsesTemporaryFile = true; sTemporaryFileUseCount++; } } else { // write back to user's xml and use it for gViewerInput filename = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, filename_default); // Don't reset mUsesTemporaryFile, it will be reset at cleanup stage } LLXMLNodePtr output_node = new LLXMLNode("keys", false); LLXUIParser parser; parser.writeXUI(output_node, keys); // Write the resulting XML to file if (!output_node->isNull()) { LLFILE *fp = LLFile::fopen(filename, "w"); if (fp != NULL) { LLXMLNode::writeHeaderToFile(fp); output_node->writeToFile(fp); fclose(fp); } } // Now force a rebind for keyboard if (gDirUtilp->fileExists(filename)) { // Ideally instead of rebinding immediately we should shedule // the rebind since single file can have multiple handlers, // one per mode, saving simultaneously. // Or whatever uses LLKeyConflictHandler should control the process. gViewerInput.loadBindingsXML(filename); } } } if (!temporary) { // will remove any temporary file if there were any clearUnsavedChanges(); } } LLKeyData LLKeyConflictHandler::getDefaultControl(const std::string &control_name, U32 index) { if (control_name.empty()) { return LLKeyData(); } if (mLoadMode == MODE_SAVED_SETTINGS) { LLControlVariablePtr var = gSavedSettings.getControl(control_name); if (var) { return LLKeyBind(var->getDefault()).getKeyData(index); } return LLKeyData(); } else { control_map_t::iterator iter = mDefaultsMap.find(control_name); if (iter != mDefaultsMap.end()) { return iter->second.mKeyBind.getKeyData(index); } return LLKeyData(); } } void LLKeyConflictHandler::resetToDefault(const std::string &control_name, U32 index) { if (control_name.empty()) { return; } LLKeyData data = getDefaultControl(control_name, index); if (data != mControlsMap[control_name].getKeyData(index)) { // reset controls that might have been switched to our current control removeConflicts(data, mControlsMap[control_name].mConflictMask); mControlsMap[control_name].setKeyData(data, index); } } void LLKeyConflictHandler::resetToDefaultAndResolve(const std::string &control_name, bool ignore_conflicts) { if (control_name.empty()) { return; } if (mLoadMode == MODE_SAVED_SETTINGS) { LLControlVariablePtr var = gSavedSettings.getControl(control_name); if (var) { LLKeyBind bind(var->getDefault()); if (!ignore_conflicts) { for (S32 i = 0; i < bind.getDataCount(); ++i) { removeConflicts(bind.getKeyData(i), mControlsMap[control_name].mConflictMask); } } mControlsMap[control_name].mKeyBind = bind; } else { mControlsMap[control_name].mKeyBind.clear(); } } else { control_map_t::iterator iter = mDefaultsMap.find(control_name); if (iter != mDefaultsMap.end()) { if (!ignore_conflicts) { for (S32 i = 0; i < iter->second.mKeyBind.getDataCount(); ++i) { removeConflicts(iter->second.mKeyBind.getKeyData(i), mControlsMap[control_name].mConflictMask); } } mControlsMap[control_name].mKeyBind = iter->second.mKeyBind; } else { mControlsMap[control_name].mKeyBind.clear(); } } } void LLKeyConflictHandler::resetToDefault(const std::string &control_name) { // reset specific binding without ignoring conflicts resetToDefaultAndResolve(control_name, false); } void LLKeyConflictHandler::resetToDefaults(ESourceMode mode) { if (mode == MODE_SAVED_SETTINGS) { control_map_t::iterator iter = mControlsMap.begin(); control_map_t::iterator end = mControlsMap.end(); for (; iter != end; ++iter) { resetToDefaultAndResolve(iter->first, true); } } else { mControlsMap.clear(); generatePlaceholders(mode); mControlsMap.insert(mDefaultsMap.begin(), mDefaultsMap.end()); } mHasUnsavedChanges = true; } void LLKeyConflictHandler::resetToDefaults() { if (!empty()) { resetToDefaults(mLoadMode); } else { // not optimal since: // 1. We are not sure that mLoadMode was set // 2. We are not sure if there are any changes in comparison to default // 3. We are loading 'current' only to replace it // but it is reliable and works Todo: consider optimizing. loadFromSettings(mLoadMode); resetToDefaults(mLoadMode); } } void LLKeyConflictHandler::clear() { if (clearUnsavedChanges()) { // temporary file was removed, this means we were using it and need to reload keyboard's bindings resetKeyboardBindings(); } mControlsMap.clear(); mDefaultsMap.clear(); } // static void LLKeyConflictHandler::resetKeyboardBindings() { // Try to load User's bindings first std::string key_bindings_file = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, filename_default); if (!gDirUtilp->fileExists(key_bindings_file) || !gViewerInput.loadBindingsXML(key_bindings_file)) { // Failed to load custom bindings, try default ones key_bindings_file = gDirUtilp->getExpandedFilename(LL_PATH_APP_SETTINGS, filename_default); if (!gViewerInput.loadBindingsXML(key_bindings_file)) { LL_ERRS("InitInfo") << "Unable to open default key bindings from " << key_bindings_file << LL_ENDL; } } } void LLKeyConflictHandler::generatePlaceholders(ESourceMode load_mode) { // These controls are meant to cause conflicts when user tries to assign same control somewhere else // also this can be used to pre-record controls that should not conflict or to assign conflict groups/masks /*registerTemporaryControl(CONTROL_RESERVED_MENU, CLICK_RIGHT, KEY_NONE, MASK_NONE, 0); registerTemporaryControl(CONTROL_DELETE, CLICK_NONE, KEY_DELETE, MASK_NONE, 0);*/ } bool LLKeyConflictHandler::removeConflicts(const LLKeyData &data, const U32 &conlict_mask) { if (conlict_mask == CONFLICT_NOTHING) { // Can't conflict return true; } std::map conflict_list; control_map_t::iterator cntrl_iter = mControlsMap.begin(); control_map_t::iterator cntrl_end = mControlsMap.end(); for (; cntrl_iter != cntrl_end; ++cntrl_iter) { S32 index = cntrl_iter->second.mKeyBind.findKeyData(data); if (index >= 0 && cntrl_iter->second.mConflictMask != CONFLICT_NOTHING && (cntrl_iter->second.mConflictMask & conlict_mask) != 0) { if (cntrl_iter->second.mAssignable) { // Potentially we can have multiple conflict flags conflicting // including unassignable keys. // So record the conflict and find all others before doing any changes. // Assume that there is only one conflict per bind conflict_list[cntrl_iter->first] = index; } else { return false; } } } std::map::iterator cnflct_iter = conflict_list.begin(); std::map::iterator cnflct_end = conflict_list.end(); for (; cnflct_iter != cnflct_end; ++cnflct_iter) { mControlsMap[cnflct_iter->first].mKeyBind.resetKeyData(cnflct_iter->second); } return true; } void LLKeyConflictHandler::registerTemporaryControl(const std::string &control_name, EMouseClickType mouse, KEY key, MASK mask, U32 conflict_mask) { LLKeyConflict *type_data = &mControlsMap[control_name]; type_data->mAssignable = false; type_data->mConflictMask = conflict_mask; type_data->mKeyBind.addKeyData(mouse, key, mask, false); } bool LLKeyConflictHandler::clearUnsavedChanges() { bool result = false; mHasUnsavedChanges = false; if (mUsesTemporaryFile) { mUsesTemporaryFile = false; sTemporaryFileUseCount--; if (!sTemporaryFileUseCount) { result = clearTemporaryFile(); } // else: might be usefull to overwrite content of temp file with defaults // but at the moment there is no such need } return result; } //static bool LLKeyConflictHandler::clearTemporaryFile() { // At the moment single file needs five handlers (one per mode), so doing this // will remove file for all hadlers std::string filename = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, filename_temporary); if (gDirUtilp->fileExists(filename)) { LLFile::remove(filename); return true; } return false; }