1013 lines
32 KiB
C++
1013 lines
32 KiB
C++
/**
|
|
* @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 "lltrans.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, bool translate)
|
|
{
|
|
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;
|
|
}
|
|
|
|
if (translate && !res.empty())
|
|
{
|
|
res = LLTrans::getString(res);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
// Example: user tried to assign camera spin to all modes, but first person mode doesn't support it
|
|
return false;
|
|
}
|
|
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;
|
|
}
|
|
|
|
bool LLKeyConflictHandler::clearControl(const std::string &control_name, U32 data_index)
|
|
{
|
|
if (control_name.empty())
|
|
{
|
|
return false;
|
|
}
|
|
LLKeyConflict &type_data = mControlsMap[control_name];
|
|
if (!type_data.mAssignable)
|
|
{
|
|
// Example: user tried to assign camera spin to all modes, but first person mode doesn't support it
|
|
return false;
|
|
}
|
|
type_data.mKeyBind.resetKeyData(data_index);
|
|
mHasUnsavedChanges = true;
|
|
return true;
|
|
}
|
|
|
|
LLKeyData LLKeyConflictHandler::getControl(const std::string &control_name, U32 index)
|
|
{
|
|
if (control_name.empty())
|
|
{
|
|
return LLKeyData();
|
|
}
|
|
return mControlsMap[control_name].getKeyData(index);
|
|
}
|
|
|
|
bool LLKeyConflictHandler::isControlEmpty(const std::string &control_name)
|
|
{
|
|
if (control_name.empty())
|
|
{
|
|
return true;
|
|
}
|
|
return mControlsMap[control_name].mKeyBind.isEmpty();
|
|
}
|
|
|
|
// 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, true);
|
|
|
|
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<LLViewerInput::KeyBinding>::const_iterator it = keymode.bindings.begin(),
|
|
end_it = keymode.bindings.end();
|
|
it != end_it;
|
|
++it)
|
|
{
|
|
KEY key;
|
|
MASK mask;
|
|
EMouseClickType mouse = CLICK_NONE;
|
|
if (it->mouse.isProvided())
|
|
{
|
|
LLViewerInput::mouseFromString(it->mouse.getValue(), &mouse);
|
|
}
|
|
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
|
|
// Assumes U32_MAX conflict mask, and is assignable by default,
|
|
// but assignability might have been overriden by generatePlaceholders.
|
|
LLKeyConflict &type_data = (*destination)[it->command];
|
|
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<decltype(saved_settings_key_controls)>::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
|
|
// Do not use mControlsMap.insert(mDefaultsMap) since mControlsMap has
|
|
// placeholders that won't be added over(to) by insert.
|
|
// Or instead move generatePlaceholders call to be after copying
|
|
control_map_t::iterator iter = mDefaultsMap.begin();
|
|
while (iter != mDefaultsMap.end())
|
|
{
|
|
mControlsMap[iter->first].mKeyBind = iter->second.mKeyBind;
|
|
iter++;
|
|
}
|
|
}
|
|
}
|
|
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 gSavedSettings
|
|
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
|
|
{
|
|
binding.key = LLKeyboard::stringFromKey(data.mKey, false /*Do not localize*/);
|
|
}
|
|
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, false), 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;
|
|
}
|
|
|
|
keys.xml_version.set(keybindings_xml_version, true);
|
|
|
|
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 (mLoadMode == MODE_THIRD_PERSON && mHasUnsavedChanges)
|
|
{
|
|
// Map floater should react to doubleclick if doubleclick for teleport is set
|
|
// Todo: Seems conterintuitive for map floater to share inworld controls,
|
|
// discuss with UI UX engineer if this should just be set to 1 by default
|
|
bool value = canHandleMouse("teleport_to", CLICK_DOUBLELEFT, MASK_NONE);
|
|
gSavedSettings.setBOOL("DoubleClickTeleport", value);
|
|
}
|
|
|
|
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;
|
|
}
|
|
LLKeyConflict &type_data = mControlsMap[control_name];
|
|
if (!type_data.mAssignable)
|
|
{
|
|
return;
|
|
}
|
|
LLKeyData data = getDefaultControl(control_name, index);
|
|
|
|
if (data != type_data.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);
|
|
mHasUnsavedChanges = true;
|
|
}
|
|
}
|
|
|
|
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::resetToDefaultsAndResolve()
|
|
{
|
|
if (mLoadMode == 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();
|
|
|
|
// Set key combinations.
|
|
// Copy from mDefaultsMap before doing generatePlaceholders, otherwise
|
|
// insert() will fail to add some keys into pre-existing values from
|
|
// generatePlaceholders()
|
|
mControlsMap.insert(mDefaultsMap.begin(), mDefaultsMap.end());
|
|
|
|
// Set conflict masks and mark functions (un)assignable
|
|
generatePlaceholders(mLoadMode);
|
|
|
|
}
|
|
|
|
mHasUnsavedChanges = true;
|
|
}
|
|
|
|
void LLKeyConflictHandler::resetToDefaults()
|
|
{
|
|
if (!empty())
|
|
{
|
|
resetToDefaultsAndResolve();
|
|
}
|
|
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);
|
|
resetToDefaultsAndResolve();
|
|
}
|
|
}
|
|
|
|
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 placeholders are meant to cause conflict resolution 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
|
|
|
|
if (load_mode == MODE_FIRST_PERSON)
|
|
{
|
|
// First person view doesn't support camera controls
|
|
// Note: might be better idea to just load these from control_table_contents_camera.xml
|
|
// or to pass from floaterpreferences when it loads said file
|
|
registerTemporaryControl("look_up");
|
|
registerTemporaryControl("look_down");
|
|
registerTemporaryControl("move_forward");
|
|
registerTemporaryControl("move_backward");
|
|
registerTemporaryControl("move_forward_fast");
|
|
registerTemporaryControl("move_backward_fast");
|
|
registerTemporaryControl("spin_over");
|
|
registerTemporaryControl("spin_under");
|
|
registerTemporaryControl("pan_up");
|
|
registerTemporaryControl("pan_down");
|
|
registerTemporaryControl("pan_left");
|
|
registerTemporaryControl("pan_right");
|
|
registerTemporaryControl("pan_in");
|
|
registerTemporaryControl("pan_out");
|
|
registerTemporaryControl("spin_around_ccw");
|
|
registerTemporaryControl("spin_around_cw");
|
|
|
|
// control_table_contents_editing.xml
|
|
registerTemporaryControl("edit_avatar_spin_ccw");
|
|
registerTemporaryControl("edit_avatar_spin_cw");
|
|
registerTemporaryControl("edit_avatar_spin_over");
|
|
registerTemporaryControl("edit_avatar_spin_under");
|
|
registerTemporaryControl("edit_avatar_move_forward");
|
|
registerTemporaryControl("edit_avatar_move_backward");
|
|
|
|
// no autopilot or teleport
|
|
registerTemporaryControl("walk_to");
|
|
registerTemporaryControl("teleport_to");
|
|
}
|
|
|
|
if (load_mode == MODE_EDIT_AVATAR)
|
|
{
|
|
// no autopilot or teleport
|
|
registerTemporaryControl("walk_to");
|
|
registerTemporaryControl("teleport_to");
|
|
}
|
|
|
|
if (load_mode == MODE_SITTING)
|
|
{
|
|
// no autopilot
|
|
registerTemporaryControl("walk_to");
|
|
}
|
|
else
|
|
{
|
|
// sitting related functions should only be avaliable in sitting mode
|
|
registerTemporaryControl("move_forward_sitting");
|
|
registerTemporaryControl("move_backward_sitting");
|
|
registerTemporaryControl("spin_over_sitting");
|
|
registerTemporaryControl("spin_under_sitting");
|
|
registerTemporaryControl("spin_around_ccw_sitting");
|
|
registerTemporaryControl("spin_around_cw_sitting");
|
|
}
|
|
|
|
|
|
// Special case, mouse clicks passed to scripts have 'lowest' piority
|
|
// thus do not conflict, everything else has a chance before them
|
|
// also in ML they have highest priority, but only when script-grabbed,
|
|
// thus do not conflict
|
|
// (see AGENT_CONTROL_ML_LBUTTON_DOWN and CONTROL_LBUTTON_DOWN_INDEX)
|
|
LLKeyConflict *type_data = &mControlsMap[script_mouse_handler_name];
|
|
type_data->mAssignable = true;
|
|
type_data->mConflictMask = U32_MAX - CONFLICT_LMOUSE;
|
|
}
|
|
|
|
bool LLKeyConflictHandler::removeConflicts(const LLKeyData &data, U32 conlict_mask)
|
|
{
|
|
if (conlict_mask == CONFLICT_NOTHING)
|
|
{
|
|
// Can't conflict
|
|
return true;
|
|
}
|
|
|
|
if (data.mMouse == CLICK_LEFT
|
|
&& data.mMask == MASK_NONE
|
|
&& data.mKey == KEY_NONE)
|
|
{
|
|
if ((conlict_mask & CONFLICT_LMOUSE) == 0)
|
|
{
|
|
// Can't conflict
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// simplify conflict mask
|
|
conlict_mask = CONFLICT_LMOUSE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// simplify conflict mask
|
|
conlict_mask &= ~CONFLICT_LMOUSE;
|
|
}
|
|
|
|
std::map<std::string, S32> 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)
|
|
{
|
|
const U32 cmp_mask = cntrl_iter->second.mConflictMask;
|
|
if ((cmp_mask & conlict_mask) == 0)
|
|
{
|
|
// can't conflict
|
|
continue;
|
|
}
|
|
S32 index = cntrl_iter->second.mKeyBind.findKeyData(data);
|
|
if (index >= 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<std::string, S32>::iterator cnflct_iter = conflict_list.begin();
|
|
std::map<std::string, S32>::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);
|
|
}
|
|
|
|
void LLKeyConflictHandler::registerTemporaryControl(const std::string &control_name, U32 conflict_mask)
|
|
{
|
|
LLKeyConflict *type_data = &mControlsMap[control_name];
|
|
type_data->mAssignable = false;
|
|
type_data->mConflictMask = conflict_mask;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|