diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index 7e3e41384c8..77e8c9f7f14 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -66,6 +66,8 @@ add_file_folder("Source/Mission/Dialogs" src/mission/dialogs/SelectionDialogModel.h src/mission/dialogs/ShieldSystemDialogModel.cpp src/mission/dialogs/ShieldSystemDialogModel.h + src/mission/dialogs/VariableDialogModel.cpp + src/mission/dialogs/VariableDialogModel.h src/mission/dialogs/WaypointEditorDialogModel.cpp src/mission/dialogs/WaypointEditorDialogModel.h ) @@ -130,6 +132,8 @@ add_file_folder("Source/UI/Dialogs" src/ui/dialogs/SelectionDialog.h src/ui/dialogs/ShieldSystemDialog.h src/ui/dialogs/ShieldSystemDialog.cpp + src/ui/dialogs/VariableDialog.cpp + src/ui/dialogs/VariableDialog.h src/ui/dialogs/VoiceActingManager.h src/ui/dialogs/VoiceActingManager.cpp src/ui/dialogs/WaypointEditorDialog.cpp @@ -201,6 +205,7 @@ add_file_folder("UI" ui/PlayerOrdersDialog.ui ui/ShipTextureReplacementDialog.ui ui/ShipTBLViewer.ui + ui/VariableDialog.ui ) add_file_folder("Resources" diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp new file mode 100644 index 00000000000..4d269d07ab2 --- /dev/null +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -0,0 +1,2407 @@ +#include "VariableDialogModel.h" +#include "parse/sexp.h" +#include +#include + +namespace fso { +namespace fred { +namespace dialogs { + static int _textMode = 0; + +VariableDialogModel::VariableDialogModel(QObject* parent, EditorViewport* viewport) + : AbstractDialogModel(parent, viewport) +{ + _deleteWarningCount = 0; + initializeData(); +} + +void VariableDialogModel::reject() +{ + _variableItems.clear(); + _containerItems.clear(); +} + +bool VariableDialogModel::checkValidModel() +{ + std::unordered_set namesTaken; + std::unordered_set duplicates; + + int emptyVarNames = 0; + + for (const auto& variable : _variableItems){ + if (!namesTaken.insert(variable.name).second) { + duplicates.insert(variable.name); + } + + if (variable.name.empty()){ + ++emptyVarNames; + } + } + + SCP_string messageOut; + SCP_string messageBuffer; + + if (!duplicates.empty()){ + for (const auto& item : duplicates){ + if (messageBuffer.empty()){ + messageBuffer = "\"" + item + "\""; + } else { + messageBuffer += ", ""\"" + item + "\""; + } + } + + sprintf(messageOut, "There are %zu duplicate variable names:\n", duplicates.size()); + messageOut += messageBuffer + "\n\n"; + } + + duplicates.clear(); + std::unordered_set namesTakenContainer; + SCP_vector duplicateKeys; + int emptyContainerNames = 0; + int emptyKeys = 0; + int notNumberKeys = 0; + + for (const auto& container : _containerItems){ + if (!namesTakenContainer.insert(container.name).second) { + duplicates.insert(container.name); + } + + if (container.name.empty()){ + ++emptyContainerNames; + } + + if (!container.list){ + std::unordered_set keysTakenContainer; + + for (const auto& key : container.keys){ + if (!keysTakenContainer.insert(key).second) { + SCP_string temp = "\"" + key + "\" in map \"" + container.name + "\", "; + duplicateKeys.push_back(temp); + } + + if (key.empty()){ + ++emptyKeys; + } else if (!container.stringKeys){ + if (key != trimIntegerString(key)){ + ++notNumberKeys; + } + + } + } + } + } + + messageBuffer.clear(); + + if (!duplicates.empty()){ + for (const auto& item : duplicates){ + if (messageBuffer.empty()){ + messageBuffer = "\"" + item + "\""; + } else { + messageBuffer += ", ""\"" + item + "\""; + } + } + + SCP_string temp; + + sprintf(temp, "There are %zu duplicate containers:\n\n", duplicates.size()); + messageOut += temp + messageBuffer + "\n"; + } + + messageBuffer.clear(); + + if (!duplicateKeys.empty()){ + for (const auto& key : duplicateKeys){ + messageBuffer += key; + } + + SCP_string temp; + + sprintf(temp, "There are %zu duplicate map keys:\n\n", duplicateKeys.size()); + messageOut += temp + messageBuffer + "\n"; + } + + if (emptyVarNames > 0){ + messageBuffer.clear(); + sprintf(messageBuffer, "There are %i empty variable names which must be populated.\n", emptyVarNames); + + messageOut += messageBuffer; + } + + if (emptyContainerNames > 0){ + messageBuffer.clear(); + sprintf(messageBuffer, "There are %i empty container names which must be populated.\n", emptyContainerNames); + + messageOut += messageBuffer; + } + + if (emptyKeys > 0){ + messageBuffer.clear(); + sprintf(messageBuffer, "There are %i empty keys which must be populated.\n", emptyKeys); + + messageOut += messageBuffer; + } + + if (notNumberKeys > 0){ + messageBuffer.clear(); + sprintf(messageBuffer, "There are %i numeric keys that are not numbers.\n", notNumberKeys); + + messageOut += messageBuffer; + } + + if (_variableItems.size() >= MAX_SEXP_VARIABLES){ + messageOut += "There are more than the max of 250 variables.\n"; + } + + + if (messageOut.empty()){ + return true; + } else { + messageOut = "Please correct these issues. The editor cannot apply your changes until they are fixed:\n\n" + messageOut; + + QMessageBox msgBox; + msgBox.setText(messageOut.c_str()); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.exec(); + + return false; + } +} + +sexp_container VariableDialogModel::createContainer(const containerInfo& infoIn) +{ + sexp_container containerOut; + + containerOut.container_name = infoIn.name; + + // handle type info, which defaults to List + if (!infoIn.list) { + containerOut.type &= ~ContainerType::LIST; + containerOut.type |= ContainerType::MAP; + + // Map Key type. This is not set by default, so we have explicity set it. + if (infoIn.stringKeys){ + containerOut.type |= ContainerType::STRING_KEYS; + } else { + containerOut.type |= ContainerType::NUMBER_KEYS; + } + } + + // New Containers also default to string data + if (!infoIn.string){ + containerOut.type &= ~ContainerType::STRING_DATA; + containerOut.type |= ContainerType::NUMBER_DATA; + } + + // Now flags + if (infoIn.flags & SEXP_VARIABLE_NETWORK){ + containerOut.type |= ContainerType::NETWORK; + } + + + // No persistence means No flag, which is the default, but if anything else is true, then this has to be + if (infoIn.flags & SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE){ + containerOut.type |= ContainerType::SAVE_ON_MISSION_CLOSE; + + if (infoIn.flags & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE){ + containerOut.type |= ContainerType::SAVE_TO_PLAYER_FILE; + } + } else if (infoIn.flags & SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS){ + containerOut.type |= ContainerType::SAVE_ON_MISSION_PROGRESS; + + if (infoIn.flags & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE){ + containerOut.type |= ContainerType::SAVE_TO_PLAYER_FILE; + } + } else { + containerOut.type &= ~ContainerType::SAVE_TO_PLAYER_FILE; + } + + + // Handle contained data + if (infoIn.list){ + + if (infoIn.string){ + for (const auto& string : infoIn.stringValues){ + containerOut.list_data.push_back(string); + } + } else { + for (const auto& number : infoIn.numberValues){ + + containerOut.list_data.push_back(std::to_string(number)); + + } + } + } else { + for (int x = 0; x < static_cast(infoIn.keys.size()); ++x){ + if (infoIn.string){ + containerOut.map_data[infoIn.keys[x]] = infoIn.stringValues[x]; + } else { + containerOut.map_data[infoIn.keys[x]] = std::to_string(infoIn.numberValues[x]); + } + } + } + + return containerOut; +} + +bool VariableDialogModel::apply() +{ + // what did we delete from the original list? We need to check these references and clean them. + SCP_vector> nameChangedVariables; + bool found; + + // first we have to edit known variables. + for (auto& variable : _variableItems){ + found = false; + + // set of instructions for updating variables + if (!variable.originalName.empty()) { + for (int i = 0; i < MAX_SEXP_VARIABLES; ++i) { + if (!stricmp(Sexp_variables[i].variable_name, variable.originalName.c_str())){ + if (variable.deleted) { + sexp_variable_delete(i); + } else { + if (variable.name != variable.originalName) { + nameChangedVariables.emplace_back(i, variable.originalName); + } + + strcpy_s(Sexp_variables[i].variable_name, variable.name.c_str()); + Sexp_variables[i].type = variable.flags; + + if (variable.flags & SEXP_VARIABLE_STRING){ + strcpy_s(Sexp_variables[i].text, variable.stringValue.c_str()); + Sexp_variables[i].type |= SEXP_VARIABLE_STRING; + } else { + strcpy_s(Sexp_variables[i].text, std::to_string(variable.numberValue).c_str()); + Sexp_variables[i].type |= SEXP_VARIABLE_NUMBER; + } + } + + found = true; + break; + } + } + + // just in case + if (!found) { + if (variable.string){ + variable.flags |= SEXP_VARIABLE_STRING; + sexp_add_variable(variable.stringValue.c_str(), variable.name.c_str(), variable.flags, -1); + } else { + variable.flags |= SEXP_VARIABLE_NUMBER; + sexp_add_variable(std::to_string(variable.numberValue).c_str(), variable.name.c_str(), variable.flags, -1); + } + } + + } else { + if (variable.string){ + variable.flags |= SEXP_VARIABLE_STRING; + sexp_add_variable(variable.stringValue.c_str(), variable.name.c_str(), variable.flags, -1); + } else { + variable.flags |= SEXP_VARIABLE_NUMBER; + sexp_add_variable(std::to_string(variable.numberValue).c_str(), variable.name.c_str(), variable.flags, -1); + } + } + } + + + SCP_vector newContainers; + SCP_unordered_map renamedContainers; + + for (const auto& container : _containerItems){ + newContainers.push_back(createContainer(container)); + + if (container.originalName != "" && container.name != container.originalName){ + renamedContainers[container.originalName] = container.name; + } + } + + update_sexp_containers(newContainers, renamedContainers); + + return true; +} + +void VariableDialogModel::initializeData() +{ + _variableItems.clear(); + _containerItems.clear(); + + for (int i = 0; i < MAX_SEXP_VARIABLES; ++i){ + if (!(Sexp_variables[i].type & SEXP_VARIABLE_NOT_USED)) { + _variableItems.emplace_back(); + auto& item = _variableItems.back(); + item.name = Sexp_variables[i].variable_name; + item.originalName = item.name; + + if (Sexp_variables[i].type & SEXP_VARIABLE_STRING) { + item.string = true; + item.stringValue = Sexp_variables[i].text; + item.numberValue = 0; + } else { + item.string = false; + + try { + item.numberValue = std::stoi(Sexp_variables[i].text); + } + catch (...) { + item.numberValue = 0; + } + + item.stringValue = ""; + } + } + } + + const auto& containers = get_all_sexp_containers(); + + for (const auto& container : containers) { + _containerItems.emplace_back(); + auto& newContainer = _containerItems.back(); + + newContainer.name = container.container_name; + newContainer.originalName = newContainer.name; + newContainer.deleted = false; + + if (any(container.type & ContainerType::STRING_DATA)) { + newContainer.string = true; + } else if (any(container.type & ContainerType::NUMBER_DATA)) { + newContainer.string = false; + } + + // using the SEXP variable version of these values here makes things easier + if (any(container.type & ContainerType::SAVE_TO_PLAYER_FILE)) { + newContainer.flags |= SEXP_VARIABLE_SAVE_TO_PLAYER_FILE; + } + + if (any(container.type & ContainerType::SAVE_ON_MISSION_CLOSE)) { + newContainer.flags |= SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE; + } + + if (any(container.type & ContainerType::SAVE_ON_MISSION_PROGRESS)) { + newContainer.flags |= SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS; + } + + if (any(container.type & ContainerType::NETWORK)) { + newContainer.flags |= SEXP_VARIABLE_NETWORK; + } + + newContainer.list = container.is_list(); + + if (any(container.type & ContainerType::LIST)) { + for (const auto& item : container.list_data){ + if (any(container.type & ContainerType::STRING_DATA)){ + newContainer.stringValues.push_back(item); + } else { + try { + newContainer.numberValues.push_back(std::stoi(item)); + } + catch (...){ + newContainer.numberValues.push_back(0); + } + } + } + } else { + if (any(container.type & ContainerType::STRING_KEYS)){ + newContainer.stringKeys = true; + } else { + newContainer.stringKeys = false; + } + + for (const auto& item : container.map_data){ + newContainer.keys.push_back(item.first); + + if (any(container.type & ContainerType::STRING_DATA)){ + newContainer.stringValues.push_back(item.second); + newContainer.numberValues.push_back(0); + } else { + newContainer.stringValues.push_back(""); + + try{ + newContainer.numberValues.push_back(std::stoi(item.second)); + } + catch (...){ + newContainer.numberValues.push_back(0); + } + } + } + } + } +} + +// true on string, false on number +bool VariableDialogModel::getVariableType(int index) +{ + auto variable = lookupVariable(index); + return (variable) ? (variable->string) : true; +} + +bool VariableDialogModel::getVariableNetworkStatus(int index) +{ + auto variable = lookupVariable(index); + return (variable) ? ((variable->flags & SEXP_VARIABLE_NETWORK) != 0) : false; +} + + +// 0 neither, 1 on mission complete, 2 on mission close (higher number saves more often) +int VariableDialogModel::getVariableOnMissionCloseOrCompleteFlag(int index) +{ + auto variable = lookupVariable(index); + + if (!variable) { + return 0; + } + + int returnValue = 0; + + if (variable->flags & SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE) + returnValue = 2; + else if (variable->flags & SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS) + returnValue = 1; + + return returnValue; +} + +bool VariableDialogModel::getVariableEternalFlag(int index) +{ + auto variable = lookupVariable(index); + return (variable) ? ((variable->flags & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE) != 0) : false; +} + + +SCP_string VariableDialogModel::getVariableStringValue(int index) +{ + auto variable = lookupVariable(index); + return (variable && variable->string) ? (variable->stringValue) : ""; +} + +int VariableDialogModel::getVariableNumberValue(int index) +{ + auto variable = lookupVariable(index); + return (variable && !variable->string) ? (variable->numberValue) : 0; +} + + + +// true on string, false on number +bool VariableDialogModel::setVariableType(int index, bool string) +{ + auto variable = lookupVariable(index); + + // nothing to change, or invalid entry + // Best way to say that it failed is to say + // that it is not switching to what the ui asked for. + if (!variable || variable->string == string){ + return !string; + } + + if (!safeToAlterVariable(index)){ + return variable->string; + } + + // Here we change the variable type! + // this variable is currently a string + if (variable->string) { + // no risk change, because no string was specified. + if (variable->stringValue == "") { + variable->string = string; + return variable->string; + } else { + // if there was no previous number value + if (variable->numberValue == 0){ + try { + variable->numberValue = std::stoi(variable->stringValue); + } + // nothing to do here, because that just means we can't convert and we have to use the old value. + catch (...) {} + + } + + variable->string = string; + return variable->string; + } + + // this variable is currently a number + } else { + // safe change because there was no number value specified + if (variable->numberValue == 0){ + variable->string = string; + return variable->string; + } else { + // if there was no previous string value + if (variable->stringValue == ""){ + sprintf(variable->stringValue, "%i", variable->numberValue); + } + + variable->string = string; + return variable->string; + } + } +} + +bool VariableDialogModel::setVariableNetworkStatus(int index, bool network) +{ + auto variable = lookupVariable(index); + + // nothing to change, or invalid entry + if (!variable){ + return false; + } + + if (!(variable->flags & SEXP_VARIABLE_NETWORK) && network){ + variable->flags |= SEXP_VARIABLE_NETWORK; + } else { + variable->flags &= ~SEXP_VARIABLE_NETWORK; + } + return network; +} + +int VariableDialogModel::setVariableOnMissionCloseOrCompleteFlag(int index, int flags) +{ + auto variable = lookupVariable(index); + + // nothing to change, or invalid entry + if (!variable || flags < 0 || flags > 2){ + return 0; + } + + if (flags == 0) { + variable->flags &= ~(SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS | SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE); + } else if (flags == 1) { + variable->flags &= ~SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE; + variable->flags |= SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS; + } else { + variable->flags |= (SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS | SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE); + } + + return flags; +} + +bool VariableDialogModel::setVariableEternalFlag(int index, bool eternal) +{ + auto variable = lookupVariable(index); + + // nothing to change, or invalid entry + if (!variable){ + return false; + } + + if (eternal) { + variable->flags |= SEXP_VARIABLE_SAVE_TO_PLAYER_FILE; + } else { + variable->flags &= ~SEXP_VARIABLE_SAVE_TO_PLAYER_FILE; + } + + return eternal; +} + +SCP_string VariableDialogModel::setVariableStringValue(int index, SCP_string value) +{ + auto variable = lookupVariable(index); + + // nothing to change, or invalid entry + if (!variable || !variable->string){ + return ""; + } + + if (!safeToAlterVariable(index)){ + return variable->stringValue; + } + + variable->stringValue = value; + return value; +} + +int VariableDialogModel::setVariableNumberValue(int index, int value) +{ + auto variable = lookupVariable(index); + + // nothing to change, or invalid entry + if (!variable || variable->string){ + return 0; + } + + if (!safeToAlterVariable(index)){ + return variable->numberValue; + } + + variable->numberValue = value; + + return value; +} + +SCP_string VariableDialogModel::addNewVariable() +{ + variableInfo* variable = nullptr; + int count = 1; + SCP_string name; + + if (atMaxVariables()){ + return ""; + } + + do { + name = ""; + sprintf(name, "newVar%i", count); + variable = lookupVariableByName(name); + ++count; + } while (variable != nullptr && count < MAX_SEXP_VARIABLES); + + if (variable){ + return ""; + } + + _variableItems.emplace_back(); + _variableItems.back().name = name; + return name; +} + +SCP_string VariableDialogModel::addNewVariable(SCP_string nameIn) +{ + if (atMaxVariables()){ + return ""; + } + + _variableItems.emplace_back(); + _variableItems.back().name.substr(0, TOKEN_LENGTH - 1) = nameIn; + return _variableItems.back().name; +} + +SCP_string VariableDialogModel::changeVariableName(int index, SCP_string newName) +{ + auto variable = lookupVariable(index); + + // nothing to change, or invalid entry + if (!variable){ + return ""; + } + + if (!safeToAlterVariable(index)){ + return variable->name; + } + + // Truncate name if needed + if (newName.length() >= TOKEN_LENGTH){ + newName = newName.substr(0, TOKEN_LENGTH - 1); + } + + // We cannot have two variables with the same name, but we need to check this somewhere else (like on accept attempt). + variable->name = newName; + return newName; +} + +SCP_string VariableDialogModel::copyVariable(int index) +{ + auto variable = lookupVariable(index); + + // nothing to change, or invalid entry + if (!variable){ + return ""; + } + + if (atMaxVariables()){ + return ""; + } + + int count = 1; + variableInfo* variableSearch; + SCP_string newName; + + do { + sprintf(newName, "%s_%i", variable->name.substr(0, TOKEN_LENGTH - 4).c_str(), count); + variableSearch = lookupVariableByName(newName); + + // open slot found! + if (!variableSearch){ + // create the new entry in the model + variableInfo newInfo; + + // and set everything as a copy from the original, except original name and deleted. + newInfo.name = newName; + newInfo.flags = variable->flags; + newInfo.string = variable->string; + + if (newInfo.string) { + newInfo.stringValue = variable->stringValue; + } else { + newInfo.numberValue = variable->numberValue; + } + + _variableItems.push_back(std::move(newInfo)); + + return newName; + } + + ++count; + } while (variableSearch != nullptr && count < MAX_SEXP_VARIABLES); + + return ""; +} + +// returns whether it succeeded +bool VariableDialogModel::removeVariable(int index, bool toDelete) +{ + auto variable = lookupVariable(index); + + // nothing to change, or invalid entry + if (!variable){ + return false; + } + + if (variable->deleted == toDelete || !safeToAlterVariable(index)){ + return variable->deleted; + } + + if (toDelete){ + if (_deleteWarningCount < 2){ + + SCP_string question = "Are you sure you want to delete this variable? Any references to it will have to be changed."; + SCP_string info = ""; + + if (!confirmAction(question, info)){ + --_deleteWarningCount; + return variable->deleted; + } + + // adjust to the user's actions. If they are deleting variable after variable, allow after a while. No one expects Cybog the Careless + ++_deleteWarningCount; + } + + variable->deleted = toDelete; + return variable->deleted; + + } else { + variable->deleted = toDelete; + return variable->deleted; + } +} + +bool VariableDialogModel::safeToAlterVariable(int index) +{ + auto variable = lookupVariable(index); + if (!variable){ + return false; + } + + // FIXME! until we can actually count references (via a SEXP backend), this is the best way to go. + if (variable->originalName != ""){ + return true; + } + + return true; +} + +bool VariableDialogModel::safeToAlterVariable(const variableInfo& variableItem) +{ + // again, FIXME! Needs actally reference count. + return variableItem.originalName == ""; +} + + +// Container Section + +// true on string, false on number +bool VariableDialogModel::getContainerValueType(int index) +{ + auto container = lookupContainer(index); + return (container) ? container->string : true; +} + +bool VariableDialogModel::getContainerKeyType(int index) +{ + auto container = lookupContainer(index); + return (container) ? container->stringKeys : true; +} + +// true on list, false on map +bool VariableDialogModel::getContainerListOrMap(int index) +{ + auto container = lookupContainer(index); + return (container) ? container->list : true; +} + +bool VariableDialogModel::getContainerNetworkStatus(int index) +{ + auto container = lookupContainer(index); + return (container) ? ((container->flags & SEXP_VARIABLE_NETWORK) != 0) : false; +} + +// 0 neither, 1 on mission complete, 2 on mission close (higher number saves more often) +int VariableDialogModel::getContainerOnMissionCloseOrCompleteFlag(int index) +{ + auto container = lookupContainer(index); + + if (!container) { + return 0; + } + + if (container->flags & SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE) + return 2; + else if (container->flags & SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS) + return 1; + else + return 0; +} + +bool VariableDialogModel::getContainerEternalFlag(int index) +{ + auto container = lookupContainer(index); + return (container) ? ((container->flags & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE) != 0) : false; +} + + +bool VariableDialogModel::setContainerValueType(int index, bool type) +{ + auto container = lookupContainer(index); + + if (!container){ + return true; + } + + if (container->string == type || !safeToAlterContainer(index)){ + return container->string; + } + + if ((container->string && container->stringValues.empty()) || (!container->string && container->numberValues.empty())){ + container->string = type; + return container->string; + } + + // if the other list is not empty, then just convert. No need to confirm. + // The values will be there if they decide to switch back. + if (container->string && !container->numberValues.empty()){ + + container->string = type; + return container->string; + + } else if (!container->string && !container->stringValues.empty()){ + + container->string = type; + return container->string; + } + + // so when the other list *is* empty, then we can attempt to copy values. + if (container->string && container->numberValues.empty()){ + + SCP_string question = "Do you want to attempt conversion of these string values to number values?"; + SCP_string info = "Your string values will still be there if you convert this container back to string type."; + + if (confirmAction(question, info)) { + + bool transferable = true; + SCP_vector numbers; + + for (const auto& item : container->stringValues){ + try { + numbers.push_back(stoi(item)); + } + catch(...) { + transferable = false; + break; + } + } + + if (transferable){ + container->numberValues = std::move(numbers); + } + } + + // now that we've handled value conversion, convert the container + container->string = type; + return container->string; + } else if (!container->string && container->stringValues.empty()){ + + SCP_string question = "Do you want to convert these number values to string values?"; + SCP_string info = "Your number values will still be there if you convert this container back to number type."; + + if (confirmAction(question, info)) { + for (const auto& item : container->numberValues){ + container->stringValues.emplace_back(std::to_string(item)); + } + } + + // now that we've handled value conversion, convert the container + container->string = type; + return container->string; + } + + // we shouldn't get here, but if we do return the current value because that's what the model thinks, anyway. + return container->string; +} + +bool VariableDialogModel::setContainerKeyType(int index, bool string) +{ + auto container = lookupContainer(index); + + // nothing to change, or invalid entry + if (!container){ + return false; + } + + if (container->stringKeys == string || !safeToAlterContainer(index)){ + return container->stringKeys; + } + + if (container->stringKeys) { + // Ok, this is the complicated type. First check if all keys can just quickly be transferred to numbers. + bool quickConvert = true; + + for (auto& key : container->keys) { + if(key != trimIntegerString(key)){ + quickConvert = false; + break; + } + } + + // Don't even notify the user. Switching back is exceedingly easy. + if (quickConvert) { + container->stringKeys = string; + return container->stringKeys; + } + + // If we couldn't convert easily, then we need some input from the user + // now ask about data + QMessageBox msgBoxContainerKeyTypeSwitch; + msgBoxContainerKeyTypeSwitch.setWindowTitle("Key Type Conversion"); + msgBoxContainerKeyTypeSwitch.setText("Fred could not convert all string keys to numbers automatically. Would you like to use default keys, filter out integers from the current keys or cancel the operation?"); + msgBoxContainerKeyTypeSwitch.setInformativeText("Current keys will be overwritten unless you cancel and cannot be restored. Filtering will keep *any* numerical digits and starting \"-\" in the string. Filtering also does not prevent duplicate keys."); + msgBoxContainerKeyTypeSwitch.addButton("Use Default Keys", QMessageBox::ActionRole); // No, these categories don't make sense, but QT makes underlying assumptions about where each button will be + msgBoxContainerKeyTypeSwitch.addButton("Filter Current Keys ", QMessageBox::RejectRole); + auto defaultButton = msgBoxContainerKeyTypeSwitch.addButton("Cancel", QMessageBox::HelpRole); + msgBoxContainerKeyTypeSwitch.setDefaultButton(defaultButton); + msgBoxContainerKeyTypeSwitch.exec(); + auto ret = msgBoxContainerKeyTypeSwitch.buttonRole(msgBoxContainerKeyTypeSwitch.clickedButton()); + + switch(ret){ + // just use default keys + case QMessageBox::ActionRole: + { + int current = 0; + for (auto& key : container->keys){ + sprintf(key, "%i", current); + ++current; + } + + container->stringKeys = string; + return container->stringKeys; + } + + // filter out current keys + case QMessageBox::RejectRole: + for (auto& key: container->keys){ + key = trimIntegerString(key); + } + + container->stringKeys = string; + return container->stringKeys; + + // cancel the operation + case QMessageBox::HelpRole: + return !string; + default: + UNREACHABLE("Bad button value from confirmation message box in the Variable editor, please report!"); + } + + } else { + // transferring to keys to string type. This can just change because a valid number is always a valid string. + container->stringKeys = string; + return container->stringKeys; + } + + return false; +} + +// This is the most complicated function, because we need to query the user on what they want to do if the had already entered data. +bool VariableDialogModel::setContainerListOrMap(int index, bool list) +{ + auto container = lookupContainer(index); + + // nothing to change, or invalid entry + if (!container){ + return !list; + } + + if (container->list == list || !safeToAlterContainer(index)){ + return container->list; + } + + if (container->list) { + // no data to either transfer to map/purge/ignore + if (container->string && container->stringValues.empty()){ + container->list = list; + + // still need to deal with extant keys by resizing data values. + if (!container->keys.empty()){ + container->stringValues.resize(container->keys.size()); + } + + return list; + } else if (!container->string && container->numberValues.empty()){ + container->list = list; + + // still need to deal with extant keys by resizing data values. + if (!container->keys.empty()){ + container->numberValues.resize(container->keys.size()); + } + + return list; + } + + QMessageBox msgBoxListToMapConfirm; + msgBoxListToMapConfirm.setText("This list already has data. Continue conversion to map?"); + msgBoxListToMapConfirm.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel); + msgBoxListToMapConfirm.setDefaultButton(QMessageBox::Cancel); + int ret = msgBoxListToMapConfirm.exec(); + + switch (ret) { + case QMessageBox::Yes: + break; + + case QMessageBox::Cancel: + return container->list; + break; + + default: + UNREACHABLE("Bad button value from confirmation message box in the Variable editor, please report!"); + return false; + break; + } + + // now ask about data + QMessageBox msgBoxListToMapRetainData; + msgBoxListToMapRetainData.setWindowTitle("List to Map Conversion"); + msgBoxListToMapRetainData.setText("Would you to keep the list data as keys or values, or would you like to purge the container contents?"); + msgBoxListToMapRetainData.setInformativeText("Converting to keys will erase current keys and cannot be undone. Purging all container data cannot be undone."); + msgBoxListToMapRetainData.addButton("Keep as Values", QMessageBox::ActionRole); // No, these categories don't make sense, but QT makes underlying assumptions about where each button will be + msgBoxListToMapRetainData.addButton("Convert to Keys", QMessageBox::RejectRole); // Instead of putting them in order of input to the + msgBoxListToMapRetainData.addButton("Purge", QMessageBox::ApplyRole); + auto defaultButton = msgBoxListToMapRetainData.addButton("Cancel", QMessageBox::HelpRole); + msgBoxListToMapRetainData.setDefaultButton(defaultButton); + msgBoxListToMapRetainData.exec(); + ret = msgBoxListToMapRetainData.buttonRole(msgBoxListToMapRetainData.clickedButton()); + + switch (ret) { + case QMessageBox::RejectRole: + // The easy version. (I know ... I should have standardized all storage as strings internally.... Now I'm in too deep) + if (container->string){ + container->keys = container->stringValues; + container->stringValues.clear(); + container->stringValues.resize(container->keys.size(), ""); + container->list = list; + return container->list; + } + + // The hard version ...... I guess it's not that bad, actually + container->keys.clear(); + + for (auto& number : container->numberValues){ + SCP_string temp; + sprintf(temp, "%i", number); + container->keys.push_back(temp); + } + + container->numberValues.clear(); + container->numberValues.resize(container->keys.size(), 0); + container->list = list; + return container->list; + break; + + case QMessageBox::ActionRole: + { + auto currentSize = (container->string) ? container->stringValues.size() : container->numberValues.size(); + + // Keys and data are already set to the correct sizes. Key type should persist from the last time it was a map, so no need + // to adjust keys. + if (currentSize == container->keys.size()) { + container->list = list; + + // we need all key related vectors to be size synced + if (container->string){ + container->numberValues.resize(container->keys.size(), 0); + } else { + container->stringValues.resize(container->keys.size(), ""); + } + + return container->list; + } + + // not enough data items. + if (currentSize < container->keys.size()) { + // just put the default value in them. Any string I specify for string values will + // be inconvenient to someone. Zero is a good default, too. + container->stringValues.resize(container->keys.size(), ""); + container->numberValues.resize(container->keys.size(), 0); + + } else { + // here currentSize must be greater than the key size, because we already dealt with equal size. + // So let's add a few keys to make them level. + int keyIndex = 0; + + while (currentSize > container->keys.size()) { + SCP_string newKey; + + if (container->stringKeys) { + sprintf(newKey, "key%i", keyIndex); + } + else { + sprintf(newKey, "%i", keyIndex); + } + + // avoid duplicates + if (!lookupContainerKeyByName(index, newKey)) { + container->keys.push_back(newKey); + } + + ++keyIndex; + } + } + + container->list = list; + return container->list; + } + break; + + case QMessageBox::ApplyRole: + + container->list = list; + container->stringValues.clear(); + container->numberValues.clear(); + container->keys.clear(); + return container->list; + break; + + case QMessageBox::HelpRole: + return !list; + break; + + default: + UNREACHABLE("Bad button value from confirmation message box in the Variable editor, please report!"); + return false; + break; + + } + } else { + // why yes, in this case it really is that simple. It doesn't matter what keys are doing, and there should already be valid values. + container->list = list; + return container->list; + } + + return !list; +} + +bool VariableDialogModel::setContainerNetworkStatus(int index, bool network) +{ + auto container = lookupContainer(index); + + // nothing to change, or invalid entry + if (!container){ + return false; + } + + if (network) { + container->flags |= SEXP_VARIABLE_NETWORK; + } else { + container->flags &= ~SEXP_VARIABLE_NETWORK; + } + + return network; +} + +int VariableDialogModel::setContainerOnMissionCloseOrCompleteFlag(int index, int flags) +{ + auto container = lookupContainer(index); + + // nothing to change, or invalid entry + if (!container || flags < 0 || flags > 2){ + return 0; + } + + if (flags == 0) { + container->flags &= ~(SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS | SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE); + } else if (flags == 1) { + container->flags &= ~(SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE); + container->flags |= SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS; + } else { + container->flags |= (SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS | SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE); + } + + return flags; +} + +bool VariableDialogModel::setContainerEternalFlag(int index, bool eternal) +{ + auto container = lookupContainer(index); + + // nothing to change, or invalid entry + if (!container){ + return false; + } + + if (eternal) { + container->flags |= SEXP_VARIABLE_SAVE_TO_PLAYER_FILE; + } else { + container->flags &= ~SEXP_VARIABLE_SAVE_TO_PLAYER_FILE; + } + + return eternal; +} + +SCP_string VariableDialogModel::addContainer() +{ + containerInfo* container = nullptr; + int count = 1; + SCP_string name; + + do { + name = ""; + sprintf(name, "newCont%i", count); + container = lookupContainerByName(name); + ++count; + } while (container != nullptr && count < 51); + + if (container){ + return ""; + } + + _containerItems.emplace_back(); + _containerItems.back().name = name; + return _containerItems.back().name; +} + +SCP_string VariableDialogModel::addContainer(SCP_string nameIn) +{ + _containerItems.emplace_back(); + _containerItems.back().name = nameIn.substr(0, TOKEN_LENGTH - 1); + return _containerItems.back().name; +} + +SCP_string VariableDialogModel::copyContainer(int index) +{ + auto container = lookupContainer(index); + + // nothing to copy, invalid entry + if (!container){ + return ""; + } + + // searching for a duplicate is not that hard. + _containerItems.push_back(*container); + container = &_containerItems.back(); + + SCP_string newName; + int count = 0; + + while(true) { + sprintf(newName, "%s_%i", container->name.substr(0, TOKEN_LENGTH - 4).c_str(), count); + auto containerSearch = lookupContainerByName(newName); + + // open slot found! + if (!containerSearch){ + break; + } + ++count; + } + + _containerItems.back().name = newName; + return _containerItems.back().name; +} + +SCP_string VariableDialogModel::changeContainerName(int index, SCP_string newName) +{ + auto container = lookupContainer(index); + + // nothing to change, or invalid entry + if (!container || !safeToAlterContainer(index)){ + return ""; + } + + // We cannot have two containers with the same name, but we need to check that somewhere else (like on accept attempt). + // Otherwise editing variables and containers becomes super annoying. + container->name = newName.substr(0, TOKEN_LENGTH - 1); + return container->name; +} + +bool VariableDialogModel::removeContainer(int index, bool toDelete) +{ + auto container = lookupContainer(index); + + if (!container){ + return false; + } + + if (container->deleted == toDelete || !safeToAlterContainer(index)){ + return container->deleted; + } + + if (toDelete){ + + if (_deleteWarningCount < 3){ + SCP_string question = "Are you sure you want to delete this container? Any references to it will have to be changed."; + SCP_string info = ""; + + if (!confirmAction(question, info)){ + return container->deleted; + } + + // adjust to the user's actions. If they are deleting container after container, allow after a while. + ++_deleteWarningCount; + } + + container->deleted = toDelete; + return container->deleted; + + } else { + container->deleted = toDelete; + return container->deleted; + } +} + +SCP_string VariableDialogModel::addListItem(int index) +{ + auto container = lookupContainer(index); + + if (!container){ + return ""; + } + + if (container->string) { + container->stringValues.emplace_back("New_Item"); + return container->stringValues.back(); + } else { + container->numberValues.push_back(0); + return "0"; + } +} + +SCP_string VariableDialogModel::addListItem(int index, SCP_string item) +{ + auto container = lookupContainer(index); + + if (!container){ + return ""; + } + + if (container->string) { + container->stringValues.push_back(item.substr(0, TOKEN_LENGTH - 1)); + return container->stringValues.back(); + } else { + auto temp = trimIntegerString(item); + + try { + int tempNumber = std::stoi(temp); + container->numberValues.push_back(tempNumber); + } + catch(...){ + container->numberValues.push_back(0); + return "0"; + } + + sprintf(temp, "%i", container->numberValues.back()); + return temp; + } +} + +std::pair VariableDialogModel::addMapItem(int index) +{ + auto container = lookupContainer(index); + + std::pair ret = {"", ""}; + + // no container available + if (!container){ + return ret; + } + + bool conflict; + int count = 0; + SCP_string newKey; + + do { + conflict = false; + + if (container->stringKeys){ + sprintf(newKey, "key%i", count); + } else { + sprintf(newKey, "%i", count); + } + + for (int x = 0; x < static_cast(container->keys.size()); ++x) { + if (container->keys[x] == newKey){ + conflict = true; + break; + } + } + + ++count; + } while (conflict && count < 101); + + if (conflict) { + return ret; + } + + ret.first = newKey; + container->keys.push_back(newKey); + + if (container->string){ + ret.second = ""; + } else { + ret.second = "0"; + } + + container->stringValues.push_back(""); + container->numberValues.push_back(0); + + sortMap(index); + + return ret; +} + +// Overload for specified key and/or Value +std::pair VariableDialogModel::addMapItem(int index, SCP_string key, SCP_string value) +{ + auto container = lookupContainer(index); + + std::pair ret = { "", "" }; + + // no container available + if (!container) { + return ret; + } + + bool conflict = false; + int count = 0; + SCP_string newKey; + + if (key.empty()) { + do { + conflict = false; + + if (container->stringKeys){ + sprintf(newKey, "key%i", count); + } else { + sprintf(newKey, "%i", count); + } + + for (int x = 0; x < static_cast(container->keys.size()); ++x) { + if (container->keys[x] == newKey){ + conflict = true; + break; + } + } + + ++count; + } while (conflict && count < 101); + } else { + if (container->stringKeys){ + newKey = key.substr(0, TOKEN_LENGTH - 1); + } else { + newKey = trimIntegerString(key); + } + } + + if (conflict) { + return ret; + } + + ret.first = newKey; + container->keys.push_back(ret.first); + + if (container->string) { + ret.second = value.substr(0, TOKEN_LENGTH - 1); + container->stringValues.push_back(ret.second); + container->numberValues.push_back(0); + } else { + try { + ret.second = trimIntegerString(value); + container->numberValues.push_back(std::stoi(ret.second)); + ret.second = value; + } + catch (...) { + ret.second = "0"; + container->numberValues.push_back(0); + } + + container->stringValues.emplace_back(""); + } + + sortMap(index); + return ret; +} + +SCP_string VariableDialogModel::copyListItem(int containerIndex, int index) +{ + auto container = lookupContainer(containerIndex); + + if (!container || index < 0 || (container->string && index >= static_cast(container->stringValues.size())) || (!container->string && index >= static_cast(container->numberValues.size()))){ + return ""; + } + + if (container->string) { + container->stringValues.push_back(container->stringValues[index]); + return container->stringValues.back(); + } else { + container->numberValues.push_back(container->numberValues[index]); + return "0"; + } + +} + +SCP_string VariableDialogModel::changeListItem(int containerIndex, int index, SCP_string newString) +{ + auto container = lookupContainer(containerIndex); + + if (!container){ + return ""; + } + + if (container->string){ + auto listItem = lookupContainerStringItem(containerIndex, index); + + if (!listItem){ + return ""; + } + + *listItem = newString.substr(0, TOKEN_LENGTH - 1); + + } else { + auto listItem = lookupContainerNumberItem(containerIndex, index); + + if (!listItem){ + return ""; + } + + try{ + *listItem = std::stoi(trimIntegerString(newString)); + } + catch(...){ + SCP_string temp; + sprintf(temp, "%i", *listItem); + return temp; + } + } + + return ""; +} + +bool VariableDialogModel::removeListItem(int containerIndex, int index) +{ + auto container = lookupContainer(containerIndex); + + if (!container || index < 0 || (container->string && index >= static_cast(container->stringValues.size())) || (!container->string && index >= static_cast(container->numberValues.size()))){ + return false; + } + + if (_deleteWarningCount < 3){ + SCP_string question = "Are you sure you want to delete this list item? This can't be undone."; + SCP_string info = ""; + + if (!confirmAction(question, info)){ + --_deleteWarningCount; + return container->deleted; + } + + // adjust to the user's actions. If they are deleting variable after variable, allow after a while. + ++_deleteWarningCount; + } + + // Most efficient, given the situation (single deletions) + if (container->string) { + container->stringValues.erase(container->stringValues.begin() + index); + } else { + container->numberValues.erase(container->numberValues.begin() + index); + } + + return true; +} + +std::pair VariableDialogModel::copyMapItem(int index, int mapIndex) +{ + auto container = lookupContainer(index); + + // any invalid case, early return + if (!container) { + return std::make_pair("", ""); + } + + auto key = lookupContainerKey(index, mapIndex); + + if (key == nullptr) { + return std::make_pair("", ""); + } + + + + if (container->string){ + auto value = lookupContainerStringItem(index, mapIndex); + + // not a valid value. + if (value == nullptr){ + return std::make_pair("", ""); + } + + SCP_string copyValue = *value; + SCP_string baseNewKey = key->substr(0, TOKEN_LENGTH - 4); + SCP_string newKey = baseNewKey + "0"; + int count = 0; + + bool found; + + do { + found = false; + for (int y = 0; y < static_cast(container->keys.size()); ++y){ + if (container->keys[y] == newKey) { + found = true; + break; + } + } + + // attempt did not work, try next number + if (found) { + sprintf(newKey, "%s%i", baseNewKey.c_str(), ++count); + } + + } while (found && count < 999); + + // we could not generate a new key .... somehow. + if (found){ + return std::make_pair("", ""); + } + + container->keys.push_back(newKey); + container->stringValues.push_back(copyValue); + container->numberValues.push_back(0); + + sortMap(index); + return std::make_pair(newKey, copyValue); + + } else { + auto value = lookupContainerNumberItem(index, mapIndex); + + // no valid value. + if (!value){ + return std::make_pair("", ""); + } + + int copyValue = *value; + SCP_string baseNewKey = key->substr(0, TOKEN_LENGTH - 4); + SCP_string newKey = baseNewKey + "0"; + int count = 0; + + bool found; + + do { + found = false; + for (int y = 0; y < static_cast(container->keys.size()); ++y){ + if (container->keys[y] == newKey) { + found = true; + break; + } + } + + // attempt did not work, try next number + if (found) { + sprintf(newKey, "%s%i", baseNewKey.c_str(), ++count); + } + + } while (found && count < 100); + + + // we could not generate a new key .... somehow. + if (found){ + return std::make_pair("", ""); + } + + container->keys.push_back(newKey); + container->numberValues.push_back(copyValue); + container->stringValues.push_back(""); + + SCP_string temp; + sprintf(temp, "%i", copyValue); + sortMap(index); + + return std::make_pair(newKey, temp); + } + + return std::make_pair("", ""); +} + +// requires a model reload anyway, so no return value. +void VariableDialogModel::shiftListItemUp(int containerIndex, int itemIndex) +{ + auto container = lookupContainer(containerIndex); + + // handle bogus cases; < 1 is not a typo, since shifting the top item up should do nothing. + if (!container || !container->list || itemIndex < 1) { + return; + } + + // handle itemIndex out of bounds + if ( (container->string && itemIndex >= static_cast(container->stringValues.size())) + || (!container->string && itemIndex >= static_cast(container->numberValues.size())) ){ + return; + } + + // now that we know it's going to work, just swap em. + if (container->string) { + std::swap(container->stringValues[itemIndex], container->stringValues[itemIndex - 1]); + } else { + std::swap(container->numberValues[itemIndex], container->numberValues[itemIndex - 1]); + } +} + +// requires a model reload anyway, so no return value. +void VariableDialogModel::shiftListItemDown(int containerIndex, int itemIndex) +{ + auto container = lookupContainer(containerIndex); + + // handle bogus cases + if (!container || !container->list || itemIndex < 0) { + return; + } + + // handle itemIndex out of bounds. -1 is necessary. since the bottom item is cannot be moved down. + if ( (container->string && itemIndex >= static_cast(container->stringValues.size()) - 1) + || (!container->string && itemIndex >= static_cast(container->numberValues.size()) - 1) ){ + return; + } + + // now that we know it's going to work, just swap em. + if (container->string) { + std::swap(container->stringValues[itemIndex], container->stringValues[itemIndex + 1]); + } else { + std::swap(container->numberValues[itemIndex], container->numberValues[itemIndex + 1]); + } +} + +// it's really because of this feature that we need data to only be in one or the other vector for maps. +// If we attempted to maintain data automatically and there was a deletion, deleting the data in +// both of the map's data vectors might be undesired, and not deleting takes the map immediately +// out of sync. Also, just displaying both data sets would be misleading. +// We just need to tell the user that the data cannot be maintained. +bool VariableDialogModel::removeMapItem(int index, int itemIndex) +{ + auto container = lookupContainer(index); + + if (!container){ + return false; + } + // container is valid. + + auto item = lookupContainerKey(index, itemIndex); + + if (!item){ + return false; + } + // key is valid + + // double check that we want to delete + if (_deleteWarningCount < 3){ + SCP_string question = "Are you sure you want to delete this map item? This can't be undone."; + SCP_string info = ""; + + if (!confirmAction(question, info)){ + --_deleteWarningCount; + return container->deleted; + } + + // adjust to the user's actions. If they are deleting variable after variable, allow after a while. + ++_deleteWarningCount; + } + + + // Now double check that we have a data value. + if (container->string && lookupContainerStringItem(index, itemIndex)){ + container->stringValues.erase(container->stringValues.begin() + itemIndex); + } else if (!container->string && lookupContainerNumberItem(index, itemIndex)){ + container->numberValues.erase(container->numberValues.begin() + itemIndex); + } else { + return false; + } + + // if we get here, we've succeeded and it's time to erase the key and bug out + container->keys.erase(container->keys.begin() + itemIndex); + // "I'm outta here!" + return true; +} + +SCP_string VariableDialogModel::changeMapItemKey(int index, int keyRow, SCP_string newKey) +{ + auto container = lookupContainer(index); + + if (!container || container->list){ + return ""; + } + + if (container->stringKeys){ + container->keys[keyRow] = newKey.substr(0, TOKEN_LENGTH - 1); + } else { + container->keys[keyRow] = trimIntegerString(newKey); + } + + sortMap(index); + return container->keys[keyRow]; +} + +SCP_string VariableDialogModel::changeMapItemStringValue(int index, int itemIndex, SCP_string newValue) +{ + auto item = lookupContainerStringItem(index, itemIndex); + + if (!item){ + return ""; + } + + *item = newValue.substr(0, TOKEN_LENGTH - 1); + + return *item; +} + +void VariableDialogModel::swapKeyAndValues(int index) +{ + auto container = lookupContainer(index); + + // bogus cases + if (!container || container->list || !safeToAlterContainer(index)){ + return; + } + + // data type is the same as the key type + if (container->string == container->stringKeys){ + // string-string is the easiest case + if (container->string){ + std::swap(container->stringValues, container->keys); + + // Complicated + } else { + // All right, make a copy. + SCP_vector keysCopy = container->keys; + + // easy part 1 + for (int x = 0; x < static_cast(container->numberValues.size()); ++x) { + // Honestly, if we did our job correctly, this shouldn't happen, but just in case. + if (x >= static_cast(container->keys.size()) ){ + // emplacing should be sufficient since we start at index 0. + container->keys.emplace_back(); + keysCopy.emplace_back(); + } + + container->keys[x] = ""; + sprintf(container->keys[x], "%i", container->numberValues[x]); + } + + // not as easy part 2 + for (int x = 0; x < static_cast(keysCopy.size()); ++x) { + if (keysCopy[x] == ""){ + container->numberValues[x] = 0; + } else { + try { + // why *yes* it did occur to me that I made a mistake when I designed this + int temp = std::stoi(keysCopy[x]); + container->numberValues[x] = temp; + } + catch(...){ + container->numberValues[x] = 0; + } + } + } + } + // not the same types + } else { + // Ok. Because keys are always strings, it will be easier when keys are numbers, because they are underlied by strings. + if (container->string){ + // Make a copy of the keys.... + SCP_vector keysCopy = container->keys; + // make the easy transfer from stringvalues to keys. Requiring that key values change type. + container->keys = container->stringValues; + container->stringKeys = true; + + for (int x = 0; x < static_cast(keysCopy.size()); ++x){ + // This *is* likely to happen as these sizes were not in sync. + if (x >= static_cast(container->numberValues.size())){ + container->numberValues.emplace_back(); + } + + try { + // why *yes* it did occur to me that I made a mistake when I designed this + int temp = std::stoi(keysCopy[x]); + container->numberValues[x] = temp; + } + catch(...){ + container->numberValues[x] = 0; + } + } + + container->string = false; + + // so here values are numbers and keys are strings. This might actually be easier than I thought. + } else { + // Directly copy key strings to the string values + container->stringValues = container->keys; + + // Transfer the number values to a temporary string, then place that string in the keys vector + for (int x = 0; x < static_cast(container->numberValues.size()); ++x){ + // Here, this shouldn't happen, but just in case. The direct assignment above is where it could have been mis-aligned. + if (x >= static_cast(container->keys.size())){ + container->keys.emplace_back(); + } + + sprintf(container->keys[x], "%i", container->numberValues[x]); + } + + // change the types of the container keys and values. + container->string = true; + container->stringKeys = false; + } + } + + sortMap(index); +} + +bool VariableDialogModel::safeToAlterContainer(int index) +{ auto container = lookupContainer(index); + + if (!container){ + return false; + } + + // FIXME! Until there's a sexp backend, we can only check if we just created the container. + if (container->originalName != ""){ + return true; + } + + return true; +} + +bool VariableDialogModel::safeToAlterContainer(const containerInfo& containerItem) +{ + // again, FIXME! Needs actally reference count. + return containerItem.originalName == ""; +} + +SCP_string VariableDialogModel::changeMapItemNumberValue(int index, int itemIndex, int newValue) +{ + auto mapItem = lookupContainerNumberItem(index, itemIndex); + + if (!mapItem){ + return ""; + } + + *mapItem = newValue; + + SCP_string ret; + sprintf(ret, "%i", newValue); + return ret; +} + + +// These functions should only be called when the container is guaranteed to exist! +const SCP_vector& VariableDialogModel::getMapKeys(int index) +{ + auto container = lookupContainer(index); + + if (!container) { + SCP_string temp; + sprintf(temp, "getMapKeys() found that container %s does not exist.", container->name.c_str()); + throw std::invalid_argument(temp.c_str()); + } + + if (container->list) { + SCP_string temp; + sprintf(temp, "getMapKeys() found that container %s is not a map.", container->name.c_str()); + throw std::invalid_argument(temp); + } + + return container->keys; +} + +// Only call when the container is guaranteed to exist! +const SCP_vector& VariableDialogModel::getStringValues(int index) +{ + auto container = lookupContainer(index); + + if (!container) { + SCP_string temp; + sprintf(temp, "getStringValues() found that container %s does not exist.", container->name.c_str()); + throw std::invalid_argument(temp); + } + + if (!container->string) { + SCP_string temp; + sprintf(temp, "getStringValues() found that container %s does not store strings.", container->name.c_str()); + throw std::invalid_argument(temp); + } + + return container->stringValues; +} + +// Only call when the container is guaranteed to exist! +const SCP_vector& VariableDialogModel::getNumberValues(int index) +{ + auto container = lookupContainer(index); + + if (!container) { + SCP_string temp; + sprintf(temp, "getNumberValues() found that container %s does not exist.", container->name.c_str()); + throw std::invalid_argument(temp); + } + + if (container->string) { + SCP_string temp; + sprintf(temp, "getNumberValues() found that container %s does not store numbers.", container->name.c_str()); + throw std::invalid_argument(temp); + } + + return container->numberValues; +} + +const SCP_vector> VariableDialogModel::getVariableValues() +{ + SCP_vector> outStrings; + + for (const auto& item : _variableItems){ + SCP_string notes = ""; + + if (!safeToAlterVariable(item)){ + notes = "Referenced"; + } else if (item.deleted){ + notes = "To Be Deleted"; + } else if (item.originalName == ""){ + notes = "New"; + } else if ((item.string && item.stringValue == "") || (!item.string && item.numberValue == 0)){ + notes = "Default Value"; + } else if (item.name != item.originalName){ + notes = "Renamed"; + } else { + notes = "Unreferenced"; + } + + SCP_string temp; + sprintf(temp, "%i", item.numberValue); + outStrings.push_back(std::array{item.name, (item.string) ? item.stringValue : temp, notes}); + } + + return outStrings; +} + +const SCP_vector> VariableDialogModel::getContainerNames() +{ + // This logic makes the string which we use to display the type of the container, based on the specific mode we're using. + SCP_string listPrefix; + SCP_string listPostscript; + + SCP_string mapPrefix; + SCP_string mapMidScript; + SCP_string mapPostscript; + + switch (_textMode) { + case 1: + listPrefix = ""; + listPostscript = " List"; + break; + + case 2: + listPrefix = "List ("; + listPostscript = ")"; + break; + + case 3: + listPrefix = "List <"; + listPostscript = ">"; + break; + + case 4: + listPrefix = "("; + listPostscript = ")"; + break; + + case 5: + listPrefix = "<"; + listPostscript = ">"; + break; + + case 6: + listPrefix = ""; + listPostscript = ""; + break; + + + default: + // this takes care of weird cases. The logic should be simple enough to not have bugs, but just in case, switch back to default. + _textMode = 0; + listPrefix = "List of "; + listPostscript = "s"; + break; + } + + switch (_textMode) { + case 1: + mapPrefix = ""; + mapMidScript = "-keyed Map of "; + mapPostscript = " Values"; + + break; + case 2: + mapPrefix = "Map ("; + mapMidScript = ", "; + mapPostscript = ")"; + + break; + case 3: + mapPrefix = "Map <"; + mapMidScript = ", "; + mapPostscript = ">"; + + break; + case 4: + mapPrefix = "("; + mapMidScript = ", "; + mapPostscript = ")"; + + break; + case 5: + mapPrefix = "<"; + mapMidScript = ", "; + mapPostscript = ">"; + + break; + case 6: + mapPrefix = ""; + mapMidScript = ", "; + mapPostscript = ""; + + break; + + default: + _textMode = 0; + mapPrefix = "Map with "; + mapMidScript = " Keys and "; + mapPostscript = " Values"; + + break; + } + + + SCP_vector> outStrings; + + for (const auto& item : _containerItems) { + SCP_string type = ""; + SCP_string notes = ""; + + if (item.string) { + type = "String"; + } else { + type += "Number"; + } + + if (item.list){ + type = listPrefix + type + listPostscript; + + } else { + + type = mapPrefix; + + if (item.stringKeys){ + type += "String"; + } else { + type += "Number"; + } + + type += mapMidScript; + + if (item.string){ + type += "String"; + } else { + type += "Number"; + } + + type += mapPostscript; + } + + + if (!safeToAlterContainer(item)){ + notes = "Referenced"; + } else if (item.deleted) { + notes = "To Be Deleted"; + } else if (item.originalName == "") { + notes = "New"; + } else if (item.name != item.originalName){ + notes = "Renamed"; + } else if (!item.list && item.keys.empty()){ + notes = "Empty Map"; + } else if (item.list && ((item.string && item.stringValues.empty()) || (!item.string && item.numberValues.empty()))){ + notes = "Empty List"; + } else { + notes = "Unreferenced"; + } + + outStrings.push_back(std::array{item.name, type, notes}); + } + + return outStrings; +} + +void VariableDialogModel::setTextMode(int modeIn) { _textMode = modeIn;} + +void VariableDialogModel::sortMap(int index) +{ + auto container = lookupContainer(index); + + // No sorting of non maps, and no point to sort if size is less than 2 + if (container->list || static_cast(container->keys.size() < 2)){ + return; + } + + // Yes, a little inefficient, but I didn't realize this was done in the original dialog when I designed the model. + SCP_vector keyCopy = container->keys; + SCP_vector sortedStringValues; + SCP_vector sortedNumberValues; + + // code borrowed from jg18, but going to try simple sorting first. Just need to see what it does with numbers. + if (container->string) { + std::sort(container->keys.begin(), container->keys.end()); + } else { + std::sort(container->keys.begin(), + container->keys.end(), + [](const SCP_string &str1, const SCP_string &str2) -> bool { + try{ + return std::atoi(str1.c_str()) < std::atoi(str2.c_str()); + } + catch(...){ + // we're up the creek if this happens anyway. + return true; + } + } + ); + } + + int y = 0; + + for (int x = 0; x < static_cast(container->keys.size()); ++x){ + // look for the first match in the temporary copy. + for (; y < static_cast(keyCopy.size()); ++y){ + // copy the values over. + if (container->keys[x] == keyCopy[y]){ + sortedStringValues.push_back(container->stringValues[y]); + sortedNumberValues.push_back(container->numberValues[y]); + break; + } + } + + // only reset y if we *dont* have a duplicate key coming up next. The first part of this check is simply a bound check. + // If the last item is a duplicate, that was checked on the previous iteration. + if ((x >= static_cast(container->keys.size()) - 1) || container->keys[x] != container->keys[x + 1]){ + y = 0; + } else { + ++y; + } + } + + Assertion(container->keys.size() == sortedStringValues.size(), "Keys size %zu and values %zu have a size mismatch after sorting. Please report to the SCP.", container->keys.size(), sortedStringValues.size()); + container->stringValues = std::move(sortedStringValues); + container->numberValues = std::move(sortedNumberValues); +} + +bool VariableDialogModel::atMaxVariables() +{ + if (_variableItems.size() < MAX_SEXP_VARIABLES){ + return false; + } + + int count = 0; + + for (const auto& item : _variableItems){ + if (!item.deleted){ + ++count; + } + } + + if (count < MAX_SEXP_VARIABLES){ + return false; + } else { + return true; + } +} + +// This function is for cleaning up input strings that should be numbers. We could use std::stoi, +// but this helps to not erase the entire string if user ends up mistyping just one digit. +// If we ever allowed float types in sexp variables ... *shudder* ... we would definitely need a float +// version of this cleanup. +SCP_string VariableDialogModel::trimIntegerString(SCP_string source) +{ + SCP_string ret; + bool foundNonZero = false; + // I was tempted to prevent exceeding the max length of the destination c-string here, but no integer + // can exceed the 31 digit limit. And we *will* have an integer at the end of this. + + // filter out non-numeric digits + std::copy_if(source.begin(), source.end(), std::back_inserter(ret), + [&foundNonZero, &ret](char c) -> bool { + switch (c) { + // ignore leading zeros. If all digits are zero, this will be handled elsewhere + case '0': + if (foundNonZero) + return true; + else + return false; + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + foundNonZero = true; + return true; + break; + // only copy the '-' char if it is the first thing to be copied. + case '-': + if (ret.empty()){ + return true; + } else { + return false; + } + default: + return false; + break; + } + } + ); + + // -0 as a string value is not a possible edge case because if we haven't found a digit, we don't copy zero. + // "-" is possible and could be zero, however, and an empty string that should be zero is possible as well. + if (ret.empty() || ret == "-"){ + ret = "0"; + } + + // here we deal with overflow values. + try { + // some OS's will deal with this properly. + long test = std::stol(ret); + + if (test > INT_MAX) { + return "2147483647"; + } else if (test < INT_MIN) { + return "-2147483648"; + } + + return ret; + } + // Others will not, sadly. + // So down here, we can still return the right overflow values if stol derped out. Since we've already cleaned out non-digits, + // checking for length *really should* allow us to know if something overflowed + catch (...){ + if (ret.size() > 10 && ret[0] == '-'){ + return "-2147483648"; + } else if (ret.size() > 9) { + return "2147483647"; + } + + // emergency return value + return "0"; + } +} + + +} // dialogs +} // fred +} // fso diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h new file mode 100644 index 00000000000..9dac7b37b17 --- /dev/null +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -0,0 +1,266 @@ +#pragma once + +#include "globalincs/pstypes.h" + +#include "AbstractDialogModel.h" +#include "parse/sexp_container.h" +#include + +namespace fso { +namespace fred { +namespace dialogs { + +struct variableInfo { + SCP_string name = ""; + SCP_string originalName = ""; + bool deleted = false; + bool string = true; + int flags = 0; + int numberValue = 0; + SCP_string stringValue = ""; +}; + + +struct containerInfo { + SCP_string name = ""; + bool deleted = false; + bool list = true; + bool string = true; + bool stringKeys = true; + int flags = 0; + + // this will allow us to look up the original values used in the mission previously. + SCP_string originalName = ""; + + // I found out that keys could be strictly typed as numbers *after* finishing the majority of the model.... + // So I am just going to store numerical keys as strings and use a bool to differentiate. + // Additionally the reason why these are separate and not in a map is to allow duplicates that the user can fix. + // Less friction than a popup telling them they did it wrong. + SCP_vector keys; + SCP_vector numberValues; + SCP_vector stringValues; +}; + +class VariableDialogModel : public AbstractDialogModel { +public: + VariableDialogModel(QObject* parent, EditorViewport* viewport); + + // true on string, false on number + bool getVariableType(int index); + bool getVariableNetworkStatus(int index); + // 0 neither, 1 on mission complete, 2 on mission close (higher number saves more often) + int getVariableOnMissionCloseOrCompleteFlag(int index); + bool getVariableEternalFlag(int index); + + SCP_string getVariableStringValue(int index); + int getVariableNumberValue(int index); + + // !! Note an innovation: when getting a request to set a value, + // this model will return the value that sticks and then will overwrite + // the value in the dialog. This means that we don't have to have the UI + // repopulate the whole editor on each change. + + // true on string, false on number + bool setVariableType(int index, bool string); + bool setVariableNetworkStatus(int index, bool network); + int setVariableOnMissionCloseOrCompleteFlag(int index, int flags); + bool setVariableEternalFlag(int index, bool eternal); + + SCP_string setVariableStringValue(int index, SCP_string value); + int setVariableNumberValue(int index, int value); + + SCP_string addNewVariable(); + SCP_string addNewVariable(SCP_string nameIn); + SCP_string changeVariableName(int index, SCP_string newName); + SCP_string copyVariable(int index); + // returns whether it succeeded + bool removeVariable(int index, bool toDelete); + bool safeToAlterVariable(int index); + bool safeToAlterVariable(const variableInfo& variableItem); + + // Container Section + + // true on string, false on number + bool getContainerValueType(int index); + // true on string, false on number -- this returns nonsense if it's not a map, please use responsibly! + bool getContainerKeyType(int index); + // true on list, false on map + bool getContainerListOrMap(int index); + bool getContainerNetworkStatus(int index); + // 0 neither, 1 on mission complete, 2 on mission close (higher number saves more often) + int getContainerOnMissionCloseOrCompleteFlag(int index); + bool getContainerEternalFlag(int index); + + bool setContainerValueType(int index, bool type); + bool setContainerKeyType(int index, bool string); + bool setContainerListOrMap(int index, bool list); + bool setContainerNetworkStatus(int index, bool network); + int setContainerOnMissionCloseOrCompleteFlag(int index, int flags); + bool setContainerEternalFlag(int index, bool eternal); + + SCP_string addContainer(); + SCP_string addContainer(SCP_string nameIn); + SCP_string copyContainer(int index); + SCP_string changeContainerName(int index, SCP_string newName); + bool removeContainer(int index, bool toDelete); + + SCP_string addListItem(int index); + SCP_string addListItem(int index, SCP_string item); + SCP_string copyListItem(int containerIndex, int index); + bool removeListItem(int containerindex, int index); + + std::pair addMapItem(int index); + std::pair addMapItem(int index, SCP_string key, SCP_string value); + std::pair copyMapItem(int index, int itemIndex); + SCP_string changeListItem(int containerIndex, int index, SCP_string newString); + bool removeMapItem(int index, int rowIndex); + + void shiftListItemUp(int containerIndex, int itemIndex); + void shiftListItemDown(int containerIndex, int itemIndex); + + SCP_string changeMapItemKey(int index, int keyIndex, SCP_string newKey); + SCP_string changeMapItemStringValue(int index, int itemIndex, SCP_string newValue); + SCP_string changeMapItemNumberValue(int index, int itemIndex, int newValue); + + const SCP_vector& getMapKeys(int index); + const SCP_vector& getStringValues(int index); + const SCP_vector& getNumberValues(int index); + + void swapKeyAndValues(int index); + + bool safeToAlterContainer(int index); + bool safeToAlterContainer(const containerInfo& containerItem); + + const SCP_vector> getVariableValues(); + const SCP_vector> getContainerNames(); + void setTextMode(int modeIn); + + bool checkValidModel(); + + bool apply() override; + void reject() override; + + void initializeData(); + + static SCP_string trimIntegerString(SCP_string source); + +private: + SCP_vector _variableItems; + SCP_vector _containerItems; + + int _deleteWarningCount; + + sexp_container createContainer(const containerInfo& infoIn); + + void sortMap(int index); + bool atMaxVariables(); + + variableInfo* lookupVariable(int index){ + if(index > -1 && index < static_cast(_variableItems.size()) ){ + return &_variableItems[index]; + } + + return nullptr; + } + + variableInfo* lookupVariableByName(SCP_string name){ + for (int x = 0; x < static_cast(_variableItems.size()); ++x) { + if (_variableItems[x].name == name) { + return &_variableItems[x]; + } + } + + return nullptr; + } + + containerInfo* lookupContainer(int index){ + if(index > -1 && index < static_cast(_containerItems.size()) ){ + return &_containerItems[index]; + } + + return nullptr; + } + + containerInfo* lookupContainerByName(SCP_string name){ + for (int x = 0; x < static_cast(_containerItems.size()); ++x) { + if (_containerItems[x].name == name) { + return &_containerItems[x]; + } + } + + return nullptr; + } + + SCP_string* lookupContainerKey(int containerIndex, int itemIndex){ + if(containerIndex > -1 && containerIndex < static_cast(_containerItems.size()) ){ + if (itemIndex > -1 && itemIndex < static_cast(_containerItems[containerIndex].keys.size())){ + return &_containerItems[containerIndex].keys[itemIndex]; + } + } + + return nullptr; + } + + SCP_string* lookupContainerKeyByName(int containerIndex, SCP_string keyIn){ + if(containerIndex > -1 && containerIndex < static_cast(_containerItems.size()) ){ + for (auto key = _containerItems[containerIndex].keys.begin(); key != _containerItems[containerIndex].keys.end(); ++key) { + if (*key == keyIn){ + return &(*key); + } + } + } + + return nullptr; + } + + SCP_string* lookupContainerStringItem(int containerIndex, int itemIndex){ + if(containerIndex > -1 && containerIndex < static_cast(_containerItems.size()) ){ + if (itemIndex > -1 && itemIndex < static_cast(_containerItems[containerIndex].stringValues.size())){ + return &_containerItems[containerIndex].stringValues[itemIndex]; + } + } + + return nullptr; + } + + int* lookupContainerNumberItem(int containerIndex, int itemIndex){ + if(containerIndex > -1 && containerIndex < static_cast(_containerItems.size()) ){ + if (itemIndex > -1 && itemIndex < static_cast(_containerItems[containerIndex].numberValues.size())){ + return &_containerItems[containerIndex].numberValues[itemIndex]; + } + } + + return nullptr; + } + + + // many of the controls in this editor can lead to drastic actions, so this will be very useful. + bool confirmAction(SCP_string question, SCP_string informativeText) + { + QMessageBox msgBox; + msgBox.setText(question.c_str()); + msgBox.setInformativeText(informativeText.c_str()); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Cancel); + int ret = msgBox.exec(); + + switch (ret) { + case QMessageBox::Yes: + return true; + break; + case QMessageBox::Cancel: + return false; + break; + default: + UNREACHABLE("Bad return value from confirmation message box in the Loadout dialog editor."); + return false; + break; + } + } + +}; + +} // namespace dialogs +} // namespace fred +} // namespace fso + diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index 3b6fa4cf483..81001acb97e 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include "mission/Editor.h" @@ -751,6 +752,11 @@ void FredView::on_actionLoadout_triggered(bool) { auto editorDialog = new dialogs::LoadoutDialog(this, _viewport); editorDialog->show(); } +void FredView::on_actionVariables_triggered(bool) { + auto editorDialog = new dialogs::VariableDialog(this, _viewport); + editorDialog->show(); +} + DialogButton FredView::showButtonDialog(DialogType type, const SCP_string& title, const SCP_string& message, diff --git a/qtfred/src/ui/FredView.h b/qtfred/src/ui/FredView.h index 573c5b06be1..a5b39b5dba2 100644 --- a/qtfred/src/ui/FredView.h +++ b/qtfred/src/ui/FredView.h @@ -43,6 +43,9 @@ class FredView: public QMainWindow, public IDialogProvider { void newMission(); + // this can be triggered by the loadout dialog and so needs to be public + void on_actionVariables_triggered(bool); + private slots: void on_actionSave_As_triggered(bool); void on_actionSave_triggered(bool); diff --git a/qtfred/src/ui/dialogs/LoadoutDialog.cpp b/qtfred/src/ui/dialogs/LoadoutDialog.cpp index 9e730551c21..6e9b2a2945b 100644 --- a/qtfred/src/ui/dialogs/LoadoutDialog.cpp +++ b/qtfred/src/ui/dialogs/LoadoutDialog.cpp @@ -514,9 +514,9 @@ void LoadoutDialog::onClearAllUsedWeaponsPressed() _lastSelectionChanged = USED_WEAPONS; } -// TODO! Finish writing a trigger to open that dialog, once the variable editor is created void LoadoutDialog::openEditVariablePressed() { + reinterpret_cast(parent())->on_actionVariables_triggered(true); } void LoadoutDialog::onSelectionRequiredPressed() diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp new file mode 100644 index 00000000000..ef73dbd6682 --- /dev/null +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -0,0 +1,1873 @@ +#include "VariableDialog.h" +#include "ui_VariableDialog.h" + +#include +#include +#include + +#include +#include +//#include + +namespace fso { +namespace fred { +namespace dialogs { + +VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) + : QDialog(parent), ui(new Ui::VariableEditorDialog()), _model(new VariableDialogModel(this, viewport)), _viewport(viewport) +{ + this->setFocus(); + ui->setupUi(this); + resize(QDialog::sizeHint()); // The best I can tell without some research, when a dialog doesn't use an underlying grid or layout, it needs to be resized this way before anything will show up + + // Major Changes, like Applying the model, rejecting changes and updating the UI. + // Here we need to check that there are no issues with variable names or container names, or with maps having duplicate keys. + connect(ui->OkCancelButtons, &QDialogButtonBox::accepted, this, &VariableDialog::checkValidModel); + // Reject if the user wants to. + connect(ui->OkCancelButtons, &QDialogButtonBox::rejected, this, &VariableDialog::reject); + connect(this, &QDialog::accepted, _model.get(), &VariableDialogModel::apply); + + connect(ui->variablesTable, + &QTableWidget::itemChanged, + this, + &VariableDialog::onVariablesTableUpdated); + + connect(ui->variablesTable, + &QTableWidget::itemSelectionChanged, + this, + &VariableDialog::onVariablesSelectionChanged); + + connect(ui->containersTable, + &QTableWidget::itemChanged, + this, + &VariableDialog::onContainersTableUpdated); + + connect(ui->containersTable, + &QTableWidget::itemSelectionChanged, + this, + &VariableDialog::onContainersSelectionChanged); + + connect(ui->containerContentsTable, + &QTableWidget::itemChanged, + this, + &VariableDialog::onContainerContentsTableUpdated); + + connect(ui->containerContentsTable, + &QTableWidget::itemSelectionChanged, + this, + &VariableDialog::onContainerContentsSelectionChanged); + + connect(ui->addVariableButton, + &QPushButton::clicked, + this, + &VariableDialog::onAddVariableButtonPressed); + + connect(ui->copyVariableButton, + &QPushButton::clicked, + this, + &VariableDialog::onCopyVariableButtonPressed); + + connect(ui->deleteVariableButton, + &QPushButton::clicked, + this, + &VariableDialog::onDeleteVariableButtonPressed); + + connect(ui->setVariableAsStringRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSetVariableAsStringRadioSelected); + + connect(ui->setVariableAsNumberRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSetVariableAsNumberRadioSelected); + + connect(ui->doNotSaveVariableRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onDoNotSaveVariableRadioSelected); + + connect(ui->saveVariableOnMissionCompletedRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSaveVariableOnMissionCompleteRadioSelected); + + connect(ui->saveVariableOnMissionCloseRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSaveVariableOnMissionCloseRadioSelected); + + connect(ui->networkVariableCheckbox, + &QCheckBox::clicked, + this, + &VariableDialog::onNetworkVariableCheckboxClicked); + + connect(ui->setVariableAsEternalcheckbox, + &QCheckBox::clicked, + this, + &VariableDialog::onSaveVariableAsEternalCheckboxClicked); + + connect(ui->addContainerButton, + &QPushButton::clicked, + this, + &VariableDialog::onAddContainerButtonPressed); + + connect(ui->copyContainerButton, + &QPushButton::clicked, + this, + &VariableDialog::onCopyContainerButtonPressed); + + connect(ui->deleteContainerButton, + &QPushButton::clicked, + this, + &VariableDialog::onDeleteContainerButtonPressed); + + connect(ui->setContainerAsMapRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSetContainerAsMapRadioSelected); + + connect(ui->setContainerAsListRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSetContainerAsListRadioSelected); + + connect(ui->setContainerAsStringRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSetContainerAsStringRadioSelected); + + connect(ui->setContainerAsNumberRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSetContainerAsNumberRadioSelected); + + connect(ui->setContainerKeyAsStringRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSetContainerKeyAsStringRadioSelected); + + connect(ui->setContainerKeyAsNumberRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSetContainerKeyAsNumberRadioSelected); + + connect(ui->doNotSaveContainerRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onDoNotSaveContainerRadioSelected); + + connect(ui->saveContainerOnMissionCloseRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSaveContainerOnMissionCloseRadioSelected); + + connect(ui->saveContainerOnMissionCompletedRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSaveContainerOnMissionCompletedRadioSelected); + + connect(ui->setContainerAsEternalCheckbox, + &QCheckBox::clicked, + this, + &VariableDialog::onSetContainerAsEternalCheckboxClicked); + + connect(ui->networkContainerCheckbox, + &QCheckBox::clicked, + this, + &VariableDialog::onNetworkContainerCheckboxClicked); + + connect(ui->addContainerItemButton, + &QPushButton::clicked, + this, + &VariableDialog::onAddContainerItemButtonPressed); + + connect(ui->copyContainerItemButton, + &QPushButton::clicked, + this, + &VariableDialog::onCopyContainerItemButtonPressed); + + connect(ui->deleteContainerItemButton, + &QPushButton::clicked, + this, + &VariableDialog::onDeleteContainerItemButtonPressed); + + connect(ui->shiftItemUpButton, + &QPushButton::clicked, + this, + &VariableDialog::onShiftItemUpButtonPressed); + + connect(ui->shiftItemDownButton, + &QPushButton::clicked, + this, + &VariableDialog::onShiftItemDownButtonPressed); + + connect(ui->swapKeysAndValuesButton, + &QPushButton::clicked, + this, + &VariableDialog::onSwapKeysAndValuesButtonPressed); + + connect(ui->selectFormatCombobox, + QOverload::of(&QComboBox::currentIndexChanged), + this, + &VariableDialog::onSelectFormatComboboxSelectionChanged); + + ui->variablesTable->setColumnCount(3); + ui->variablesTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); + ui->variablesTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); + ui->variablesTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); + ui->variablesTable->setColumnWidth(0, 200); + ui->variablesTable->setColumnWidth(1, 200); + ui->variablesTable->setColumnWidth(2, 130); + + ui->containersTable->setColumnCount(3); + ui->containersTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); + ui->containersTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Types")); + ui->containersTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); + ui->containersTable->setColumnWidth(0, 190); + ui->containersTable->setColumnWidth(1, 220); + ui->containersTable->setColumnWidth(2, 120); + + ui->containerContentsTable->setColumnCount(2); + + // Default to list + ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); + ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); + ui->containerContentsTable->setColumnWidth(0, 245); + ui->containerContentsTable->setColumnWidth(1, 245); + + // set radio buttons to manually toggled, as some of these have the same parent widgets and some don't + // and I don't mind just manually toggling them. + ui->setVariableAsStringRadio->setAutoExclusive(false); + ui->setVariableAsNumberRadio->setAutoExclusive(false); + ui->doNotSaveVariableRadio->setAutoExclusive(false); + ui->saveVariableOnMissionCompletedRadio->setAutoExclusive(false); + ui->saveVariableOnMissionCloseRadio->setAutoExclusive(false); + + ui->setContainerAsMapRadio->setAutoExclusive(false); + ui->setContainerAsListRadio->setAutoExclusive(false); + ui->setContainerAsStringRadio->setAutoExclusive(false); + ui->setContainerAsNumberRadio->setAutoExclusive(false); + ui->saveContainerOnMissionCloseRadio->setAutoExclusive(false); + ui->saveContainerOnMissionCompletedRadio->setAutoExclusive(false); + + ui->variablesTable->setRowCount(0); + ui->containersTable->setRowCount(0); + ui->containerContentsTable->setRowCount(0); + ui->variablesTable->clearSelection(); + ui->containersTable->clearSelection(); + ui->containerContentsTable->clearSelection(); + + ui->selectFormatCombobox->addItem("Verbose"); + ui->selectFormatCombobox->addItem("Simplified"); + ui->selectFormatCombobox->addItem("Type and ()"); + ui->selectFormatCombobox->addItem("Type and <>"); + ui->selectFormatCombobox->addItem("Only ()"); + ui->selectFormatCombobox->addItem("Only <>"); + ui->selectFormatCombobox->addItem("No extra Marks"); + + applyModel(); +} + +void VariableDialog::onVariablesTableUpdated() +{ + if (_applyingModel){ + return; + } + + int currentRow = getCurrentVariableRow(); + + if (currentRow < 0){ + return; + } + + auto item = ui->variablesTable->item(currentRow, 0); + SCP_string itemText = item->text().toStdString(); + bool apply = false; + + // This will only be true if the user is trying to add a new variable. + if (currentRow == ui->variablesTable->rowCount() - 1) { + + // make sure the item exists before we dereference + if (ui->variablesTable->item(currentRow, 0)) { + + // Add the new container. + if (!itemText.empty() && itemText != "Add Variable ...") { + _model->addNewVariable(itemText); + _currentVariable = itemText; + _currentVariableData = ""; + applyModel(); + } + + } else { + // reapply the model if the item is null. + applyModel(); + } + + // we're done here because we cannot edit the data column on the add variable row + return; + } + + // so if the user just removed the name, mark it as deleted + if (itemText.empty() && !_currentVariable.empty()) { + + _model->removeVariable(item->row(), true); + // these things need to be done whether the deletion failed or not. + _currentVariable = _model->changeVariableName(item->row(), itemText); + apply = true; + + // if the user is restoring a deleted variable by inserting a name.... + } else if (!itemText.empty() && _currentVariable.empty()){ + + _model->removeVariable(item->row(), false); + // these things need to be done whether the restoration failed or not. + _currentVariable =_model->changeVariableName(item->row(), itemText); + apply = true; + + } else if (itemText != _currentVariable){ + + auto ret = _model->changeVariableName(item->row(), itemText); + + // we put something in the cell, but the model couldn't process it. + if (strlen(item->text().toStdString().c_str()) && ret == ""){ + // update of variable name failed, resync UI + apply = true; + + // we had a successful rename. So update the variable we reference. + } else if (ret != "") { + item->setText(ret.c_str()); + _currentVariable = ret; + } + }// No action needed if the first cell was not changed. + + + // now work on the variable data cell + item = ui->variablesTable->item(currentRow, 1); + itemText = item->text().toStdString(); + + // check if data column was altered + if (itemText != _currentVariableData) { + // Variable is a string + if (_model->getVariableType(item->row())){ + SCP_string temp = itemText; + temp = temp.substr(0, NAME_LENGTH - 1); + + SCP_string ret = _model->setVariableStringValue(item->row(), temp); + if (ret == ""){ + apply = true; + } else { + item->setText(ret.c_str()); + _currentVariableData = ret; + } + + // Variable is a number + } else { + SCP_string source = item->text().toStdString(); + SCP_string temp = _model->trimIntegerString(source); + + try { + int ret = _model->setVariableNumberValue(item->row(), std::stoi(temp)); + temp = ""; + sprintf(temp, "%i", ret); + item->setText(temp.c_str()); + } + catch (...) { + // that's not good.... + apply = true; + } + + // best we can do is to set this to temp, whether conversion fails or not. + _currentContainerItemCol2 = temp; + } + } + + if (apply) { + applyModel(); + } +} + + +void VariableDialog::onVariablesSelectionChanged() +{ + if (_applyingModel){ + applyModel(); + return; + } + + int row = getCurrentVariableRow(); + + if (row < 0){ + updateVariableOptions(false); + return; + } + + SCP_string newVariableName; + + auto item = ui->variablesTable->item(row, 0); + + if (item){ + newVariableName = item->text().toStdString(); + } + + item = ui->variablesTable->item(row, 1); + + if (item){ + _currentVariableData = item->text().toStdString(); + } + + if (newVariableName != _currentVariable){ + _currentVariable = newVariableName; + } + + applyModel(); +} + + +void VariableDialog::onContainersTableUpdated() +{ + if (_applyingModel){ + applyModel(); + return; + } + + int row = getCurrentContainerRow(); + + // just in case something is goofy, return + if (row < 0){ + applyModel(); + return; + } + + // Are they adding a new container? + if (row == ui->containersTable->rowCount() - 1){ + if (ui->containersTable->item(row, 0)) { + SCP_string newString = ui->containersTable->item(row, 0)->text().toStdString(); + if (!newString.empty() && newString != "Add Container ..."){ + _model->addContainer(newString); + _currentContainer = newString; + applyModel(); + } + } + else { + applyModel(); + } + + return; + + // are they editing an existing container name? + } else if (ui->containersTable->item(row, 0)){ + SCP_string newName = ui->containersTable->item(row,0)->text().toStdString(); + + // Restoring a deleted container? + if (_currentContainer.empty()){ + _model->removeContainer(row, false); + // Removing a container? + } else if (newName.empty()) { + _model->removeContainer(row, true); + } + + _currentContainer = _model->changeContainerName(row, newName); + applyModel(); + } +} + +void VariableDialog::onContainersSelectionChanged() +{ + if (_applyingModel){ + applyModel(); + return; + } + + int row = getCurrentContainerRow(); + + if (row < 0) { + updateContainerOptions(false); + return; + } + + // guaranteed not to be null, since getCurrentContainerRow already checked. + _currentContainer = ui->containersTable->item(row, 0)->text().toStdString(); + applyModel(); +} + +void VariableDialog::onContainerContentsTableUpdated() +{ + if (_applyingModel){ + applyModel(); + return; + } + + int containerRow = getCurrentContainerRow(); + int row = getCurrentContainerItemRow(); + + + + // just in case something is goofy, return + if (row < 0 || containerRow < 0){ + applyModel(); + return; + } + + // Are they adding a new item? + if (row == ui->containerContentsTable->rowCount() - 1){ + + SCP_string newString; + + if (ui->containerContentsTable->item(row, 0)) { + newString = ui->containerContentsTable->item(row, 0)->text().toStdString(); + + if (!newString.empty() && newString != "Add item ..."){ + + if (_model->getContainerListOrMap(containerRow)) { + _model->addListItem(containerRow, newString); + } else { + _model->addMapItem(containerRow, newString, ""); + } + + _currentContainerItemCol1 = newString; + _currentContainerItemCol2 = ""; + + applyModel(); + return; + } + + } + + if (ui->containerContentsTable->item(row, 1)) { + newString = ui->containerContentsTable->item(row, 1)->text().toStdString(); + + if (!newString.empty() && newString != "Add item ..."){ + + // This should not be a list container. + if (_model->getContainerListOrMap(containerRow)) { + applyModel(); + return; + } + + auto ret = _model->addMapItem(containerRow, "", newString); + + _currentContainerItemCol1 = ret.first; + _currentContainerItemCol2 = ret.second; + + applyModel(); + return; + } + } + + // are they editing an existing container item column 1? + } else if (ui->containerContentsTable->item(row, 0)){ + SCP_string newText = ui->containerContentsTable->item(row, 0)->text().toStdString(); + + if (_model->getContainerListOrMap(containerRow)){ + + if (newText != _currentContainerItemCol1){ + + // Trim the string if necessary + if (!_model->getContainerValueType(containerRow)){ + newText = _model->trimIntegerString(newText); + } + + // Finally change the list item + _currentContainerItemCol1 = _model->changeListItem(containerRow, row, newText); + applyModel(); + return; + } + + } else if (newText != _currentContainerItemCol1){ + _model->changeMapItemKey(containerRow, row, newText); + applyModel(); + return; + } + } + + // if we're here, nothing has changed so far. So let's attempt column 2 + if (ui->containerContentsTable->item(row, 1) && !_model->getContainerListOrMap(containerRow)){ + + SCP_string newText = ui->containerContentsTable->item(row, 1)->text().toStdString(); + + if(newText != _currentContainerItemCol2){ + + if (_model->getContainerValueType(containerRow)){ + _currentContainerItemCol2 = _model->changeMapItemStringValue(containerRow, row, newText); + + } else { + try{ + _currentContainerItemCol2 = _model->changeMapItemNumberValue(containerRow, row, std::stoi(_model->trimIntegerString(newText))); + } + catch(...) { + _currentContainerItemCol2 = _model->changeMapItemNumberValue(containerRow, row, 0); + } + } + + applyModel(); + } + } +} + +void VariableDialog::onContainerContentsSelectionChanged() +{ + if (_applyingModel){ + applyModel(); + return; + } + + int row = getCurrentContainerItemRow(); + + if (row < 0){ + applyModel(); + return; + } + + auto item = ui->containerContentsTable->item(row, 0); + SCP_string newContainerItemName; + + if (!item){ + applyModel(); + return; + } + + newContainerItemName = item->text().toStdString(); + item = ui->containerContentsTable->item(row, 1); + SCP_string newContainerDataText = (item) ? item->text().toStdString() : ""; + + if (newContainerItemName != _currentContainerItemCol1 || _currentContainerItemCol2 != newContainerDataText){ + _currentContainerItemCol1 = newContainerItemName; + _currentContainerItemCol2 = newContainerDataText; + applyModel(); + } +} + +void VariableDialog::onAddVariableButtonPressed() +{ + auto ret = _model->addNewVariable(); + _currentVariable = ret; + applyModel(); +} + +void VariableDialog::onCopyVariableButtonPressed() +{ + if (_currentVariable.empty()){ + applyModel(); + return; + } + + int currentRow = getCurrentVariableRow(); + + if (currentRow < 0){ + applyModel(); + return; + } + + auto ret = _model->copyVariable(currentRow); + _currentVariable = ret; + applyModel(); +} + +void VariableDialog::onDeleteVariableButtonPressed() +{ + if (_currentVariable.empty()){ + applyModel(); + return; + } + + int currentRow = getCurrentVariableRow(); + + if (currentRow < 0){ + applyModel(); + return; + } + + // Because of the text update we'll need, this needs an applyModel, whether it fails or not. + if (ui->deleteVariableButton->text().toStdString() == "Restore") { + _model->removeVariable(currentRow, false); + applyModel(); + } else { + _model->removeVariable(currentRow, true); + applyModel(); + } +} + +void VariableDialog::onSetVariableAsStringRadioSelected() +{ + int currentRow = getCurrentVariableRow(); + + if (currentRow < 0){ + applyModel(); + return; + } + + // this doesn't return succeed or fail directly, + // but if it doesn't return true then it failed since this is the string radio + _model->setVariableType(currentRow, true); + applyModel(); +} + +void VariableDialog::onSetVariableAsNumberRadioSelected() +{ + int currentRow = getCurrentVariableRow(); + + if (currentRow < 0){ + applyModel(); + return; + } + + // this doesn't return succeed or fail directly, + // but if it doesn't return false then it failed since this is the number radio + _model->setVariableType(currentRow, false); + applyModel(); +} + +void VariableDialog::onDoNotSaveVariableRadioSelected() +{ + int currentRow = getCurrentVariableRow(); + + if (currentRow < 0 || !ui->doNotSaveVariableRadio->isChecked()){ + applyModel(); + return; + } + + int ret = _model->setVariableOnMissionCloseOrCompleteFlag(currentRow, 0); + + if (ret != 0){ + applyModel(); + } else { + ui->saveVariableOnMissionCompletedRadio->setChecked(false); + ui->saveVariableOnMissionCloseRadio->setChecked(false); + + ui->setVariableAsEternalcheckbox->setChecked(false); + ui->setVariableAsEternalcheckbox->setEnabled(false); + } +} + +void VariableDialog::onSaveVariableOnMissionCompleteRadioSelected() +{ + int row = getCurrentVariableRow(); + + if (row < 0 || !ui->saveVariableOnMissionCompletedRadio->isChecked()){ + applyModel(); + return; + } + + auto ret = _model->setVariableOnMissionCloseOrCompleteFlag(row, 1); + + if (ret != 1){ + applyModel(); + } else { + ui->doNotSaveVariableRadio->setChecked(false); + ui->saveVariableOnMissionCloseRadio->setChecked(false); + + ui->setVariableAsEternalcheckbox->setEnabled(true); + ui->setVariableAsEternalcheckbox->setChecked(_model->getVariableEternalFlag(row)); + } +} + +void VariableDialog::onSaveVariableOnMissionCloseRadioSelected() +{ + int row = getCurrentVariableRow(); + + if (row < 0 || !ui->saveVariableOnMissionCloseRadio->isChecked()){ + applyModel(); + return; + } + + auto ret = _model->setVariableOnMissionCloseOrCompleteFlag(row, 2); + + // out of sync because we did not get the expected return value. + if (ret != 2){ + applyModel(); + } else { + ui->doNotSaveVariableRadio->setChecked(false); + ui->saveVariableOnMissionCompletedRadio->setChecked(false); + + ui->setVariableAsEternalcheckbox->setEnabled(true); + ui->setVariableAsEternalcheckbox->setChecked(_model->getVariableEternalFlag(row)); + } +} + +void VariableDialog::onSaveVariableAsEternalCheckboxClicked() +{ + int row = getCurrentVariableRow(); + + if (row < 0){ + return; + } + + // If the model returns the old status, then the change failed and we're out of sync. + if (ui->setVariableAsEternalcheckbox->isChecked() == _model->setVariableEternalFlag(row, ui->setVariableAsEternalcheckbox->isChecked())) { + applyModel(); + } else { + ui->setVariableAsEternalcheckbox->setChecked(!ui->setVariableAsEternalcheckbox->isChecked()); + } +} + +void VariableDialog::onNetworkVariableCheckboxClicked() +{ + int row = getCurrentVariableRow(); + + if (row < 0){ + return; + } + + // If the model returns the old status, then the change failed and we're out of sync. + if (ui->networkVariableCheckbox->isChecked() == _model->setVariableNetworkStatus(row, ui->networkVariableCheckbox->isChecked())) { + applyModel(); + } else { + ui->networkVariableCheckbox->setChecked(!ui->networkVariableCheckbox->isChecked()); + } +} + +void VariableDialog::onAddContainerButtonPressed() +{ + auto result = (_model->addContainer()); + + if (result.empty()) { + QMessageBox msgBox; + msgBox.setText("Adding a container failed because the code is out of automatic names. Try adding a container directly in the table."); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setDefaultButton(QMessageBox::Ok); + msgBox.exec(); + } + + applyModel(); + +} + +void VariableDialog::onCopyContainerButtonPressed() +{ + int row = getCurrentContainerRow(); + + if (row < 0 ){ + return; + } + + // This will always need an apply model update, whether it succeeds or fails. + _model->copyContainer(row); + applyModel(); +} + +void VariableDialog::onDeleteContainerButtonPressed() +{ + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + // Because of the text update we'll need, this needs an applyModel, whether it fails or not. + if (ui->deleteContainerButton->text().toStdString() == "Restore"){ + _model->removeContainer(row, false); + } else { + _model->removeContainer(row, true); + } + + applyModel(); +} + +void VariableDialog::onSetContainerAsMapRadioSelected() +{ + // to avoid visual weirdness, make it false. + ui->setContainerAsListRadio->setChecked(false); + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + _model->setContainerListOrMap(row, false); + applyModel(); +} + +void VariableDialog::onSetContainerAsListRadioSelected() +{ + // to avoid visual weirdness, make it false. + ui->setContainerAsMapRadio->setChecked(false); + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + _model->setContainerListOrMap(row, true); + applyModel(); +} + + +void VariableDialog::onSetContainerAsStringRadioSelected() +{ + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + _model->setContainerValueType(row, true); + applyModel(); +} + +void VariableDialog::onSetContainerAsNumberRadioSelected() +{ + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + _model->setContainerValueType(row, false); + applyModel(); +} + +void VariableDialog::onSetContainerKeyAsStringRadioSelected() +{ + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + _model->setContainerKeyType(row, true); + applyModel(); +} + + +void VariableDialog::onSetContainerKeyAsNumberRadioSelected() +{ + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + _model->setContainerKeyType(row, false); + applyModel(); +} + +void VariableDialog::onDoNotSaveContainerRadioSelected() +{ + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + if (_model->setContainerOnMissionCloseOrCompleteFlag(row, 0) != 0){ + applyModel(); + } else { + ui->doNotSaveContainerRadio->setChecked(true); + ui->saveContainerOnMissionCloseRadio->setChecked(false); + ui->saveContainerOnMissionCompletedRadio->setChecked(false); + + ui->setContainerAsEternalCheckbox->setChecked(false); + ui->setContainerAsEternalCheckbox->setEnabled(false); + } +} + +void VariableDialog::onSaveContainerOnMissionCompletedRadioSelected() +{ + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + if (_model->setContainerOnMissionCloseOrCompleteFlag(row, 1) != 1) + applyModel(); + else { + ui->doNotSaveContainerRadio->setChecked(false); + ui->saveContainerOnMissionCloseRadio->setChecked(false); + ui->saveContainerOnMissionCompletedRadio->setChecked(true); + + ui->setContainerAsEternalCheckbox->setEnabled(true); + ui->setContainerAsEternalCheckbox->setChecked(_model->getContainerEternalFlag(row)); + } +} + +void VariableDialog::onSaveContainerOnMissionCloseRadioSelected() +{ + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + if (_model->setContainerOnMissionCloseOrCompleteFlag(row, 2) != 2) + applyModel(); + else { + ui->doNotSaveContainerRadio->setChecked(false); + ui->saveContainerOnMissionCloseRadio->setChecked(true); + ui->saveContainerOnMissionCompletedRadio->setChecked(false); + + ui->setContainerAsEternalCheckbox->setEnabled(true); + ui->setContainerAsEternalCheckbox->setChecked(_model->getContainerEternalFlag(row)); + } +} + +void VariableDialog::onNetworkContainerCheckboxClicked() +{ + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + if (ui->networkContainerCheckbox->isChecked() != _model->setContainerNetworkStatus(row, ui->networkContainerCheckbox->isChecked())){ + applyModel(); + } +} + +void VariableDialog::onSetContainerAsEternalCheckboxClicked() +{ + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + if (ui->setContainerAsEternalCheckbox->isChecked() != _model->setContainerEternalFlag(row, ui->setContainerAsEternalCheckbox->isChecked())){ + applyModel(); + } +} + +void VariableDialog::onAddContainerItemButtonPressed() +{ + int containerRow = getCurrentContainerRow(); + + if (containerRow < 0){ + applyModel(); + return; + } + + if (_model->getContainerListOrMap(containerRow)) { + _model->addListItem(containerRow); + } else { + _model->addMapItem(containerRow); + } + + applyModel(); +} + +void VariableDialog::onCopyContainerItemButtonPressed() +{ + int containerRow = getCurrentContainerRow(); + + if (containerRow < 0){ + applyModel(); + return; + } + + int itemRow = getCurrentContainerItemRow(); + + if (itemRow < 0){ + applyModel(); + return; + } + + if (_model->getContainerListOrMap(containerRow)) { + _model->copyListItem(containerRow, itemRow); + } else { + _model->copyMapItem(containerRow, itemRow); + } + + applyModel(); +} + +void VariableDialog::onDeleteContainerItemButtonPressed() +{ + int containerRow = getCurrentContainerRow(); + + if (containerRow < 0){ + applyModel(); + return; + } + + int itemRow = getCurrentContainerItemRow(); + + if (itemRow < 0){ + applyModel(); + return; + } + + if (_model->getContainerListOrMap(containerRow)) { + _model->removeListItem(containerRow, itemRow); + } else { + _model->removeMapItem(containerRow, itemRow); + } + + applyModel(); +} + +void VariableDialog::onShiftItemUpButtonPressed() +{ + int containerRow = getCurrentContainerRow(); + + if (containerRow < 0){ + applyModel(); + return; + } + + int itemRow = getCurrentContainerItemRow(); + + // item row being 0 is bad here since we're shifting up. + if (itemRow < 1){ + applyModel(); + return; + } + + _model->shiftListItemUp(containerRow, itemRow); + applyModel(); +} + +void VariableDialog::onShiftItemDownButtonPressed() +{ + int containerRow = getCurrentContainerRow(); + + if (containerRow < 0){ + applyModel(); + return; + } + + int itemRow = getCurrentContainerItemRow(); + + if (itemRow < 0){ + applyModel(); + return; + } + + _model->shiftListItemDown(containerRow, itemRow); + applyModel(); +} + +void VariableDialog::onSwapKeysAndValuesButtonPressed() +{ + int containerRow = getCurrentContainerRow(); + + if (containerRow < 0){ + applyModel(); + return; + } + + _model->swapKeyAndValues(containerRow); + applyModel(); +} + +void VariableDialog::onSelectFormatComboboxSelectionChanged() +{ + _model->setTextMode(ui->selectFormatCombobox->currentIndex()); + applyModel(); +} + +VariableDialog::~VariableDialog(){}; // NOLINT + + +void VariableDialog::applyModel() +{ + if (_applyingModel) { + return; + } + + _applyingModel = true; + + auto variables = _model->getVariableValues(); + int x = 0, selectedRow = -1; + + ui->variablesTable->setRowCount(static_cast(variables.size()) + 1); + bool safeToAlter = false; + + for (x = 0; x < static_cast(variables.size()); ++x){ + if (ui->variablesTable->item(x, 0)){ + ui->variablesTable->item(x, 0)->setText(variables[x][0].c_str()); + ui->variablesTable->item(x, 0)->setFlags(ui->variablesTable->item(x, 0)->flags() | Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem(variables[x][0].c_str()); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->variablesTable->setItem(x, 0, item); + } + + // check if this is the current variable. This keeps us selecting the correct variable even when + // there's a deletion. + if (selectedRow < 0 && !_currentVariable.empty() && variables[x][0] == _currentVariable){ + selectedRow = x; + + if (_model->safeToAlterVariable(selectedRow)){ + safeToAlter = true; + } + } + + if (ui->variablesTable->item(x, 1)){ + ui->variablesTable->item(x, 1)->setText(variables[x][1].c_str()); + ui->variablesTable->item(x, 1)->setFlags(ui->variablesTable->item(x, 1)->flags() | Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem(variables[x][1].c_str()); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->variablesTable->setItem(x, 1, item); + } + + if (ui->variablesTable->item(x, 2)){ + ui->variablesTable->item(x, 2)->setText(variables[x][2].c_str()); + ui->variablesTable->item(x, 2)->setFlags(ui->variablesTable->item(x, 2)->flags() & ~Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem(variables[x][2].c_str()); + ui->variablesTable->setItem(x, 2, item); + ui->variablesTable->item(x, 2)->setFlags(item->flags() & ~Qt::ItemIsEditable); + } + } + + // set the Add variable row + if (ui->variablesTable->item(x, 0)){ + ui->variablesTable->item(x, 0)->setText("Add Variable ..."); + } else { + QTableWidgetItem* item = new QTableWidgetItem("Add Variable ..."); + ui->variablesTable->setItem(x, 0, item); + } + + if (ui->variablesTable->item(x, 1)){ + ui->variablesTable->item(x, 1)->setFlags(ui->variablesTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); + ui->variablesTable->item(x, 1)->setText(""); + } else { + QTableWidgetItem* item = new QTableWidgetItem(""); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->variablesTable->setItem(x, 1, item); + } + + if (ui->variablesTable->item(x, 2)){ + ui->variablesTable->item(x, 2)->setFlags(ui->variablesTable->item(x, 2)->flags() & ~Qt::ItemIsEditable); + ui->variablesTable->item(x, 2)->setText(""); + } else { + QTableWidgetItem* item = new QTableWidgetItem(""); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->variablesTable->setItem(x, 2, item); + } + + if (_currentVariable.empty() || selectedRow < 0){ + if (ui->variablesTable->item(0, 0) && !ui->variablesTable->item(0, 0)->text().toStdString().empty()){ + _currentVariable = ui->variablesTable->item(0, 0)->text().toStdString(); + } + + if (ui->variablesTable->item(0, 1)) { + _currentVariableData = ui->variablesTable->item(0, 1)->text().toStdString(); + } + } + + updateVariableOptions(safeToAlter); + + auto containers = _model->getContainerNames(); + ui->containersTable->setRowCount(static_cast(containers.size() + 1)); + selectedRow = -1; + + for (x = 0; x < static_cast(containers.size()); ++x){ + if (ui->containersTable->item(x, 0)){ + ui->containersTable->item(x, 0)->setText(containers[x][0].c_str()); + ui->containersTable->item(x, 0)->setFlags(ui->containersTable->item(x, 0)->flags() | Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem(containers[x][0].c_str()); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->containersTable->setItem(x, 0, item); + } + + // check if this is the current variable. + if (selectedRow < 0 && containers[x][0] == _currentContainer){ + selectedRow = x; + } + + if (ui->containersTable->item(x, 1)){ + ui->containersTable->item(x, 1)->setText(containers[x][1].c_str()); + } else { + QTableWidgetItem* item = new QTableWidgetItem(containers[x][1].c_str()); + ui->containersTable->setItem(x, 1, item); + } + + if (ui->containersTable->item(x, 2)){ + ui->containersTable->item(x, 2)->setText(containers[x][2].c_str()); + } else { + QTableWidgetItem* item = new QTableWidgetItem(containers[x][2].c_str()); + ui->containersTable->setItem(x, 2, item); + } + } + + // do we need to switch the delete button to a restore button? + if (selectedRow > -1 && ui->containersTable->item(selectedRow, 2) && ui->containersTable->item(selectedRow, 2)->text().toStdString() == "To Be Deleted") { + ui->deleteContainerButton->setText("Restore"); + + // We can't restore empty container names. + if (ui->containersTable->item(selectedRow, 0) && ui->containersTable->item(selectedRow, 0)->text().toStdString().empty()){ + ui->deleteContainerButton->setEnabled(false); + } else { + ui->deleteContainerButton->setEnabled(true); + } + + } else { + ui->deleteContainerButton->setText("Delete"); + } + + // set the Add container row + if (ui->containersTable->item(x, 0)){ + ui->containersTable->item(x, 0)->setText("Add Container ..."); + } else { + QTableWidgetItem* item = new QTableWidgetItem("Add Container ..."); + ui->containersTable->setItem(x, 0, item); + } + + if (ui->containersTable->item(x, 1)){ + ui->containersTable->item(x, 1)->setFlags(ui->containersTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); + ui->containersTable->item(x, 1)->setText(""); + } else { + QTableWidgetItem* item = new QTableWidgetItem(""); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->containersTable->setItem(x, 1, item); + } + + if (ui->containersTable->item(x, 2)){ + ui->containersTable->item(x, 2)->setFlags(ui->containersTable->item(x, 2)->flags() & ~Qt::ItemIsEditable); + ui->containersTable->item(x, 2)->setText(""); + } else { + QTableWidgetItem* item = new QTableWidgetItem(""); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->containersTable->setItem(x, 2, item); + } + + bool safeToAlterContainer = false; + + if (selectedRow < 0 && ui->containersTable->rowCount() > 1) { + if (ui->containersTable->item(0, 0)){ + _currentContainer = ui->containersTable->item(0, 0)->text().toStdString(); + ui->containersTable->clearSelection(); + ui->containersTable->item(0, 0)->setSelected(true); + } + } else if (selectedRow > -1){ + safeToAlterContainer = _model->safeToAlterContainer(selectedRow); + } + + // this will update the list/map items. + updateContainerOptions(safeToAlterContainer); + + _applyingModel = false; +}; + +void VariableDialog::updateVariableOptions(bool safeToAlter) +{ + int row = getCurrentVariableRow(); + + if (row < 0){ + ui->copyVariableButton->setEnabled(false); + ui->deleteVariableButton->setEnabled(false); + ui->deleteVariableButton->setText("Delete"); + ui->setVariableAsStringRadio->setEnabled(false); + ui->setVariableAsNumberRadio->setEnabled(false); + ui->doNotSaveVariableRadio->setEnabled(false); + ui->saveVariableOnMissionCompletedRadio->setEnabled(false); + ui->saveVariableOnMissionCloseRadio->setEnabled(false); + ui->setVariableAsEternalcheckbox->setEnabled(false); + ui->networkVariableCheckbox->setEnabled(false); + ui->shiftItemUpButton->setEnabled(false); + ui->shiftItemDownButton->setEnabled(false); + return; + } + + // options that are always safe + ui->copyVariableButton->setEnabled(true); + ui->doNotSaveVariableRadio->setEnabled(true); + ui->saveVariableOnMissionCompletedRadio->setEnabled(true); + ui->saveVariableOnMissionCloseRadio->setEnabled(true); + ui->networkVariableCheckbox->setEnabled(true); + + // options that are only safe if there are no references + if (safeToAlter){ + ui->deleteVariableButton->setEnabled(true); + ui->setVariableAsStringRadio->setEnabled(true); + ui->setVariableAsNumberRadio->setEnabled(true); + } else { + ui->deleteVariableButton->setEnabled(false); + ui->setVariableAsStringRadio->setEnabled(false); + ui->setVariableAsNumberRadio->setEnabled(false); + } + + // start populating values + bool string = _model->getVariableType(row); + ui->setVariableAsStringRadio->setChecked(string); + ui->setVariableAsNumberRadio->setChecked(!string); + + // do we need to switch the delete button to a restore button? + if (ui->variablesTable->item(row, 2) && ui->variablesTable->item(row, 2)->text().toStdString() == "To Be Deleted"){ + ui->deleteVariableButton->setText("Restore"); + + // We can't restore empty variable names. + if (ui->variablesTable->item(row, 0) && ui->variablesTable->item(row, 0)->text().toStdString().empty()){ + ui->deleteVariableButton->setEnabled(false); + } else { + ui->deleteVariableButton->setEnabled(true); + } + + } else { + ui->deleteVariableButton->setText("Delete"); + } + + int ret = _model->getVariableOnMissionCloseOrCompleteFlag(row); + + if (ret == 0){ + ui->doNotSaveVariableRadio->setChecked(true); + ui->saveVariableOnMissionCompletedRadio->setChecked(false); + ui->saveVariableOnMissionCloseRadio->setChecked(false); + + ui->setVariableAsEternalcheckbox->setChecked(false); + ui->setVariableAsEternalcheckbox->setEnabled(false); + } else if (ret == 1) { + ui->doNotSaveVariableRadio->setChecked(false); + ui->saveVariableOnMissionCompletedRadio->setChecked(true); + ui->saveVariableOnMissionCloseRadio->setChecked(false); + + ui->setVariableAsEternalcheckbox->setEnabled(true); + ui->setVariableAsEternalcheckbox->setChecked(_model->getVariableEternalFlag(row)); + } else { + ui->setVariableAsEternalcheckbox->setEnabled(true); + ui->doNotSaveVariableRadio->setChecked(false); + ui->saveVariableOnMissionCompletedRadio->setChecked(false); + ui->saveVariableOnMissionCloseRadio->setChecked(true); + + ui->setVariableAsEternalcheckbox->setEnabled(true); + ui->setVariableAsEternalcheckbox->setChecked(_model->getVariableEternalFlag(row)); + } + + ui->networkVariableCheckbox->setChecked(_model->getVariableNetworkStatus(row)); +} + +void VariableDialog::updateContainerOptions(bool safeToAlter) +{ + int row = getCurrentContainerRow(); + + if (row < 0){ + ui->copyContainerButton->setEnabled(false); + ui->deleteContainerButton->setEnabled(false); + ui->deleteContainerButton->setText("Delete"); + ui->setContainerAsStringRadio->setEnabled(false); + ui->setContainerAsNumberRadio->setEnabled(false); + ui->setContainerKeyAsStringRadio->setEnabled(false); + ui->setContainerKeyAsNumberRadio->setEnabled(false); + ui->doNotSaveContainerRadio->setEnabled(false); + ui->saveContainerOnMissionCompletedRadio->setEnabled(false); + ui->saveContainerOnMissionCloseRadio->setEnabled(false); + ui->setContainerAsEternalCheckbox->setEnabled(false); + ui->setContainerAsMapRadio->setEnabled(false); + ui->setContainerAsListRadio->setEnabled(false); + ui->networkContainerCheckbox->setEnabled(false); + ui->shiftItemUpButton->setEnabled(false); + ui->shiftItemDownButton->setEnabled(false); + ui->swapKeysAndValuesButton->setEnabled(false); + + ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); + ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); + + + // if there's no container, there's no container items + ui->addContainerItemButton->setEnabled(false); + ui->copyContainerItemButton->setEnabled(false); + ui->deleteContainerItemButton->setEnabled(false); + ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); + ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); + ui->shiftItemDownButton->setEnabled(false); + ui->shiftItemUpButton->setEnabled(false); + ui->containerContentsTable->clearSelection(); + ui->containerContentsTable->setRowCount(0); + + + } else { + // options that should always be turned on + ui->copyContainerButton->setEnabled(true); + ui->doNotSaveContainerRadio->setEnabled(true); + ui->saveContainerOnMissionCompletedRadio->setEnabled(true); + ui->saveContainerOnMissionCloseRadio->setEnabled(true); + ui->networkContainerCheckbox->setEnabled(true); + + // options that require it be safe to alter because the container is not referenced + ui->deleteContainerButton->setEnabled(safeToAlter); + ui->setContainerAsStringRadio->setEnabled(safeToAlter); + ui->setContainerAsNumberRadio->setEnabled(safeToAlter); + ui->setContainerAsMapRadio->setEnabled(safeToAlter); + ui->setContainerAsListRadio->setEnabled(safeToAlter); + + if (_model->getContainerValueType(row)){ + ui->setContainerAsStringRadio->setChecked(true); + ui->setContainerAsNumberRadio->setChecked(false); + } else { + ui->setContainerAsStringRadio->setChecked(false); + ui->setContainerAsNumberRadio->setChecked(true); + } + + if (_model->getContainerListOrMap(row)){ + ui->setContainerAsListRadio->setChecked(true); + ui->setContainerAsMapRadio->setChecked(false); + + // Disable Key Controls + ui->setContainerKeyAsStringRadio->setEnabled(false); + ui->setContainerKeyAsNumberRadio->setEnabled(false); + + // Don't forget to change headings + ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); + ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); + + updateContainerDataOptions(true, safeToAlter); + + } else { + ui->setContainerAsListRadio->setChecked(false); + ui->setContainerAsMapRadio->setChecked(true); + + // Enable Key Controls + ui->setContainerKeyAsStringRadio->setEnabled(safeToAlter); + ui->setContainerKeyAsNumberRadio->setEnabled(safeToAlter); + + // string keys + if (_model->getContainerKeyType(row)){ + ui->setContainerKeyAsStringRadio->setChecked(true); + ui->setContainerKeyAsNumberRadio->setChecked(false); + + // number keys + } else { + ui->setContainerKeyAsStringRadio->setChecked(false); + ui->setContainerKeyAsNumberRadio->setChecked(true); + } + + // Don't forget to change headings + ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Key")); + ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); + updateContainerDataOptions(false, safeToAlter); + } + + ui->networkContainerCheckbox->setChecked(_model->getContainerNetworkStatus(row)); + + int ret = _model->getContainerOnMissionCloseOrCompleteFlag(row); + + if (ret == 0){ + ui->doNotSaveContainerRadio->setChecked(true); + ui->saveContainerOnMissionCompletedRadio->setChecked(false); + ui->saveContainerOnMissionCloseRadio->setChecked(false); + + ui->setContainerAsEternalCheckbox->setChecked(false); + ui->setContainerAsEternalCheckbox->setEnabled(false); + + } else if (ret == 1) { + ui->doNotSaveContainerRadio->setChecked(false); + ui->saveContainerOnMissionCompletedRadio->setChecked(true); + ui->saveContainerOnMissionCloseRadio->setChecked(false); + + ui->setContainerAsEternalCheckbox->setEnabled(true); + ui->setContainerAsEternalCheckbox->setChecked(_model->getContainerEternalFlag(row)); + } else { + ui->doNotSaveContainerRadio->setChecked(false); + ui->saveContainerOnMissionCompletedRadio->setChecked(false); + ui->saveContainerOnMissionCloseRadio->setChecked(true); + + ui->setContainerAsEternalCheckbox->setEnabled(true); + ui->setContainerAsEternalCheckbox->setChecked(_model->getContainerEternalFlag(row)); + } + + } +} + +void VariableDialog::updateContainerDataOptions(bool list, bool safeToAlter) +{ + int row = getCurrentContainerRow(); + + // Just in case, No overarching container, no container contents + if (row < 0){ + ui->addContainerItemButton->setEnabled(false); + ui->copyContainerItemButton->setEnabled(false); + ui->deleteContainerItemButton->setEnabled(false); + ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); + ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); + ui->containerContentsTable->setRowCount(0); + ui->shiftItemDownButton->setEnabled(false); + ui->shiftItemUpButton->setEnabled(false); + ui->swapKeysAndValuesButton->setEnabled(false); + + return; + + // list type container + } else if (list) { + // if there's no container, there's no container items + ui->addContainerItemButton->setEnabled(true); + ui->copyContainerItemButton->setEnabled(true); + ui->deleteContainerItemButton->setEnabled(true); + ui->shiftItemDownButton->setEnabled(true); + ui->shiftItemUpButton->setEnabled(true); + ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); + ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); + ui->swapKeysAndValuesButton->setEnabled(false); + + ui->containerContentsTable->setRowCount(0); + + int x; + + // with string contents + if (_model->getContainerValueType(row)){ + auto& strings = _model->getStringValues(row); + int containerItemsRow = -1; + ui->containerContentsTable->setRowCount(static_cast(strings.size()) + 1); + + + for (x = 0; x < static_cast(strings.size()); ++x){ + if (ui->containerContentsTable->item(x, 0)){ + ui->containerContentsTable->item(x, 0)->setText(strings[x].c_str()); + } else { + QTableWidgetItem* item = new QTableWidgetItem(strings[x].c_str()); + ui->containerContentsTable->setItem(x, 0, item); + } + + // set selected and enable shifting functions + if (containerItemsRow < 0 && strings[x] == _currentContainerItemCol1){ + ui->containerContentsTable->clearSelection(); + ui->containerContentsTable->item(x,0)->setSelected(true); + + // more than one item and not already at the top of the list. + if (!(x > 0 && x < static_cast(strings.size()))){ + ui->shiftItemUpButton->setEnabled(false); + } + + if (!(x > -1 && x < static_cast(strings.size()) - 1)){ + ui->shiftItemDownButton->setEnabled(false); + } + } + + // empty out the second column as it's not needed in list mode + if (ui->containerContentsTable->item(x, 1)){ + ui->containerContentsTable->item(x, 1)->setText(""); + ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem(""); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 1, item); + } + } + + // list with number contents + } else { + auto& numbers = _model->getNumberValues(row); + int containerItemsRow = -1; + ui->containerContentsTable->setRowCount(static_cast(numbers.size()) + 1); + + for (x = 0; x < static_cast(numbers.size()); ++x){ + if (ui->containerContentsTable->item(x, 0)){ + ui->containerContentsTable->item(x, 0)->setText(std::to_string(numbers[x]).c_str()); + } else { + QTableWidgetItem* item = new QTableWidgetItem(std::to_string(numbers[x]).c_str()); + ui->containerContentsTable->setItem(x, 0, item); + } + + // set selected and enable shifting functions + if (containerItemsRow < 0 ){ + + SCP_string temp; + + if (numbers[x] == 0){ + temp = "0"; + } else { + sprintf(temp, "%i", numbers[x]); + } + + if (temp == _currentContainerItemCol1){ + ui->containerContentsTable->clearSelection(); + ui->containerContentsTable->item(x,0)->setSelected(true); + + // more than one item and not already at the top of the list. + if (!(x > 0 && x < static_cast(numbers.size()))){ + ui->shiftItemUpButton->setEnabled(false); + } + + if (!(x > -1 && x < static_cast(numbers.size()) - 1)){ + ui->shiftItemDownButton->setEnabled(false); + } + } + } + + // empty out the second column as it's not needed in list mode + if (ui->containerContentsTable->item(x, 1)){ + ui->containerContentsTable->item(x, 1)->setText(""); + ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem(""); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 1, item); + } + } + } + + if (ui->containerContentsTable->item(x, 0)){ + ui->containerContentsTable->item(x, 0)->setText("Add item ..."); + ui->containerContentsTable->item(x, 0)->setFlags(ui->containerContentsTable->item(x, 0)->flags() | Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem("Add item ..."); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 0, item); + } + + if (ui->containerContentsTable->item(x, 1)){ + ui->containerContentsTable->item(x, 1)->setText(""); + ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem(""); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 1, item); + } + + // or it could be a map container + } else { + ui->addContainerItemButton->setEnabled(true); + ui->copyContainerItemButton->setEnabled(true); + ui->deleteContainerItemButton->setEnabled(true); + ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Key")); + ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); + + // Enable shift up and down buttons are off in Map mode, because order makes no difference + ui->shiftItemUpButton->setEnabled(false); + ui->shiftItemDownButton->setEnabled(false); + + // we can swap if it's safe or if the data types match. If the data types *don't* match, then we run into reference issues. + ui->swapKeysAndValuesButton->setEnabled(safeToAlter || _model->getContainerKeyType(row) == _model->getContainerValueType(row)); + + // keys I didn't bother to make separate. Should have done the same with values, ah regrets. + auto& keys = _model->getMapKeys(row); + + int x; + // string valued map. + if (_model->getContainerValueType(row)){ + auto& strings = _model->getStringValues(row); + + // use the map as the size because map containers are only as good as their keys anyway. + ui->containerContentsTable->setRowCount(static_cast(keys.size()) + 1); + + for (x = 0; x < static_cast(keys.size()); ++x){ + if (ui->containerContentsTable->item(x, 0)){ + ui->containerContentsTable->item(x, 0)->setText(keys[x].c_str()); + } else { + QTableWidgetItem* item = new QTableWidgetItem(keys[x].c_str()); + ui->containerContentsTable->setItem(x, 0, item); + } + + if (x < static_cast(strings.size())){ + if (ui->containerContentsTable->item(x, 1)){ + ui->containerContentsTable->item(x, 1)->setText(strings[x].c_str()); + ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem(strings[x].c_str()); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 1, item); + } + } + } + + // number valued map + } else { + auto& numbers = _model->getNumberValues(row); + ui->containerContentsTable->setRowCount(static_cast(keys.size()) + 1); + + for (x = 0; x < static_cast(keys.size()); ++x){ + if (ui->containerContentsTable->item(x, 0)){ + ui->containerContentsTable->item(x, 0)->setText(keys[x].c_str()); + } else { + QTableWidgetItem* item = new QTableWidgetItem(keys[x].c_str()); + ui->containerContentsTable->setItem(x, 0, item); + } + + if (x < static_cast(numbers.size())){ + if (ui->containerContentsTable->item(x, 1)){ + ui->containerContentsTable->item(x, 1)->setText(std::to_string(numbers[x]).c_str()); + ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem(std::to_string(numbers[x]).c_str()); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 1, item); + } + } else { + if (ui->containerContentsTable->item(x, 1)){ + ui->containerContentsTable->item(x, 1)->setText(""); + ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem(""); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 1, item); + } + } + } + } + + if (ui->containerContentsTable->item(x, 0)){ + ui->containerContentsTable->item(x, 0)->setText("Add item ..."); + ui->containerContentsTable->item(x, 0)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem("Add item ..."); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 0, item); + } + + if (ui->containerContentsTable->item(x, 1)){ + ui->containerContentsTable->item(x, 1)->setText("Add item ..."); + ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem("Add item ..."); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 1, item); + } + } +} + +int VariableDialog::getCurrentVariableRow() +{ + auto items = ui->variablesTable->selectedItems(); + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for (const auto& item : items) { + if (item && item->column() == 0 && item->text().toStdString() != "Add Variable ...") { + return item->row(); + } + } + + return -1; +} + +int VariableDialog::getCurrentContainerRow() +{ + auto items = ui->containersTable->selectedItems(); + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for (const auto& item : items) { + if (item && item->column() == 0 && item->text().toStdString() != "Add Container ...") { + return item->row(); + } + } + + return -1; +} + +int VariableDialog::getCurrentContainerItemRow() +{ + auto items = ui->containerContentsTable->selectedItems(); + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for (const auto& item : items) { + if (item && ((item->column() == 0 && item->text().toStdString() != "Add item ...") || (item->column() == 1 && item->text().toStdString() != "Add item ..."))) { + return item->row(); + } + } + + return -1; +} + +void VariableDialog::checkValidModel() +{ + if (ui->OkCancelButtons->button(QDialogButtonBox::Ok)->hasFocus() && _model->checkValidModel()){ + accept(); + } +} + +} // namespace dialogs +} // namespace fred +} // namespace fso \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h new file mode 100644 index 00000000000..6d37610faee --- /dev/null +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -0,0 +1,106 @@ +#pragma once + +#include +#include +#include +#include + +namespace fso { +namespace fred { +namespace dialogs { + +namespace Ui { +class VariableEditorDialog; +} + +class VariableDialog : public QDialog { + Q_OBJECT + + public: + explicit VariableDialog(FredView* parent, EditorViewport* viewport); + ~VariableDialog() override; + + private: + std::unique_ptr ui; + std::unique_ptr _model; + EditorViewport* _viewport; + + // basically UpdateUI, but called when there is an inconsistency between model and UI + void applyModel(); + void checkValidModel(); + + // Helper functions for this + void updateVariableOptions(bool safeToAlter); + void updateContainerOptions(bool safeToAlter); + void updateContainerDataOptions(bool list, bool safeToAlter); + + void onVariablesTableUpdated(); + void onVariablesSelectionChanged(); + void onContainersTableUpdated(); + void onContainersSelectionChanged(); + void onContainerContentsTableUpdated(); + void onContainerContentsSelectionChanged(); + void onAddVariableButtonPressed(); + void onDeleteVariableButtonPressed(); + void onCopyVariableButtonPressed(); + void onSetVariableAsStringRadioSelected(); + void onSetVariableAsNumberRadioSelected(); + void onDoNotSaveVariableRadioSelected(); + void onSaveVariableOnMissionCompleteRadioSelected(); + void onSaveVariableOnMissionCloseRadioSelected(); + void onSaveVariableAsEternalCheckboxClicked(); + void onNetworkVariableCheckboxClicked(); + + void onAddContainerButtonPressed(); + void onDeleteContainerButtonPressed(); + void onCopyContainerButtonPressed(); + void onSetContainerAsMapRadioSelected(); + void onSetContainerAsListRadioSelected(); + void onSetContainerAsStringRadioSelected(); + void onSetContainerAsNumberRadioSelected(); + void onSetContainerKeyAsStringRadioSelected(); + void onSetContainerKeyAsNumberRadioSelected(); + void onDoNotSaveContainerRadioSelected(); + void onSaveContainerOnMissionCloseRadioSelected(); + void onSaveContainerOnMissionCompletedRadioSelected(); + void onNetworkContainerCheckboxClicked(); + void onSetContainerAsEternalCheckboxClicked(); + void onAddContainerItemButtonPressed(); + void onCopyContainerItemButtonPressed(); + void onDeleteContainerItemButtonPressed(); + void onShiftItemUpButtonPressed(); + void onShiftItemDownButtonPressed(); + void onSwapKeysAndValuesButtonPressed(); + void onSelectFormatComboboxSelectionChanged(); + + int getCurrentVariableRow(); + int getCurrentContainerRow(); + int getCurrentContainerItemRow(); + + bool _applyingModel = false; + SCP_string _currentVariable = ""; + SCP_string _currentVariableData = ""; + SCP_string _currentContainer = ""; + SCP_string _currentContainerItemCol1 = ""; + SCP_string _currentContainerItemCol2 = ""; + + void reject() override + { + QMessageBox msgBox; + msgBox.setText("Are you sure you want to discard your changes?"); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + int ret = msgBox.exec(); + + if (ret == QMessageBox::Yes) { + QDialog::reject(); + } + } +}; + + + + + +} // namespace dialogs +} // namespace fred +} // namespace fso \ No newline at end of file diff --git a/qtfred/ui/FredView.ui b/qtfred/ui/FredView.ui index eca5d7c11de..9ffff9762ad 100644 --- a/qtfred/ui/FredView.ui +++ b/qtfred/ui/FredView.ui @@ -166,6 +166,7 @@ + @@ -1474,6 +1475,17 @@ Shift+G + + + &Variables + + + Open Variables and Containers Editor + + + Shift+V + + diff --git a/qtfred/ui/LoadoutDialog.ui b/qtfred/ui/LoadoutDialog.ui index a454de8ee09..1e8477915e2 100644 --- a/qtfred/ui/LoadoutDialog.ui +++ b/qtfred/ui/LoadoutDialog.ui @@ -2,6 +2,9 @@ fso::fred::dialogs::LoadoutDialog + + Qt::WindowModal + true diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui new file mode 100644 index 00000000000..6551fb54423 --- /dev/null +++ b/qtfred/ui/VariableDialog.ui @@ -0,0 +1,659 @@ + + + fso::fred::dialogs::VariableEditorDialog + + + Qt::WindowModal + + + + 0 + 0 + 899 + 711 + + + + + 899 + 711 + + + + + 899 + 711 + + + + Variables Editor + + + + + + + + + 734 + 643 + + + + Variables + + + + + 580 + 120 + 81 + 101 + + + + Type Options + + + + + 10 + 20 + 71 + 80 + + + + + + + String + + + + + + + Number + + + + + + + + + + 10 + 30 + 551 + 191 + + + + Qt::ScrollBarAlwaysOn + + + Qt::ScrollBarAlwaysOff + + + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed + + + false + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + Qt::DotLine + + + false + + + false + + + false + + + + + + 680 + 20 + 191 + 201 + + + + Persistence Options + + + + + 10 + 20 + 181 + 181 + + + + + + + No Persistence + + + + + + + Save on Mission Completed + + + + + + + Save on Mission Close + + + + + + + Eternal + + + + + + + Network Variable + + + + + + + + + + 580 + 30 + 82 + 86 + + + + + + + Add + + + + + + + Copy + + + + + + + Delete + + + + + + + + + 0 + 230 + 881 + 431 + + + + + 0 + 380 + + + + Containers + + + + + 10 + 210 + 661 + 211 + + + + Container Contents + + + + + 10 + 30 + 511 + 171 + + + + Qt::ScrollBarAlwaysOn + + + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed + + + false + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + Qt::DotLine + + + false + + + false + + + + + + 550 + 30 + 106 + 175 + + + + + + + Add + + + + + + + Copy + + + + + + + Shift Up + + + + + + + Shift Down + + + + + + + Swap Key/Value + + + + + + + + 8 + + + + Delete + + + + + + + + + + 580 + 30 + 82 + 85 + + + + + + + Add + + + + + + + Copy + + + + + + + + 8 + + + + Delete + + + + + + + + + 680 + 20 + 191 + 181 + + + + Persistence Options + + + + + 10 + 20 + 181 + 161 + + + + + + + No Persistence + + + + + + + Save on Mission Completed + + + + + + + Save on Mission Close + + + + + + + Eternal + + + + + + + Network Variable + + + + + + + + + + 680 + 210 + 191 + 211 + + + + Type Options + + + + + 10 + 20 + 181 + 181 + + + + + + + Container Type + + + + + + + + + List + + + + + + + Map + + + + + + + + + Key Type + + + + + + + + + String + + + + + + + Number + + + + + + + + + Data Type + + + + + + + + + String + + + + + + + Number + + + + + + + + + + + + 10 + 30 + 551 + 171 + + + + Qt::ScrollBarAlwaysOn + + + Qt::ScrollBarAlwaysOff + + + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed + + + false + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + Qt::DotLine + + + false + + + false + + + false + + + + + + 580 + 140 + 81 + 20 + + + + Type Format + + + Qt::AlignCenter + + + + + + 570 + 160 + 101 + 24 + + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + true + + + + + + + variablesTable + addVariableButton + copyVariableButton + deleteVariableButton + setVariableAsStringRadio + setVariableAsNumberRadio + doNotSaveVariableRadio + saveVariableOnMissionCompletedRadio + saveVariableOnMissionCloseRadio + setVariableAsEternalcheckbox + networkVariableCheckbox + containersTable + addContainerButton + copyContainerButton + deleteContainerButton + doNotSaveContainerRadio + saveContainerOnMissionCompletedRadio + saveContainerOnMissionCloseRadio + setContainerAsEternalCheckbox + networkContainerCheckbox + containerContentsTable + addContainerItemButton + copyContainerItemButton + shiftItemUpButton + shiftItemDownButton + swapKeysAndValuesButton + deleteContainerItemButton + setContainerAsListRadio + setContainerAsMapRadio + setContainerAsStringRadio + setContainerAsNumberRadio + setContainerKeyAsStringRadio + setContainerKeyAsNumberRadio + + + +