openclonk/src/editor/C4ConsoleQtNewScenario.cpp

554 lines
18 KiB
C++

/*
* OpenClonk, http://www.openclonk.org
*
* Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/
* Copyright (c) 2013, The OpenClonk Team and contributors
*
* Distributed under the terms of the ISC license; see accompanying file
* "COPYING" for details.
*
* "Clonk" is a registered trademark of Matthes Bender, used with permission.
* See accompanying file "TRADEMARK" for details.
*
* To redistribute this file separately, substitute the full license texts
* for the above references.
*/
/* "New scenario" editor dialogue */
#include "C4Include.h"
#include "script/C4Value.h"
#include "config/C4Config.h"
#include "editor/C4ConsoleQtNewScenario.h"
/* Definition file list model for new scenario definition selection */
C4ConsoleQtDefinitionFileListModel::DefFileInfo::DefFileInfo(C4ConsoleQtDefinitionFileListModel::DefFileInfo *parent, const char *filename, const char *root_path)
: parent(parent), filename(filename), root_path(root_path), was_opened(false), is_root(false), user_selected(parent->IsUserSelected()), force_selected(parent->IsForceSelected())
{
// Delay opening of groups until information is actually requested
// Full names into child groups in C4S always delimeted with backslashes
if (parent->full_filename.getLength())
full_filename = parent->full_filename + R"(\)" + filename;
else
full_filename = filename;
}
C4ConsoleQtDefinitionFileListModel::DefFileInfo::DefFileInfo()
{
// Init as root: List definitions in root paths
// Objects.ocd is always there (even if not actually found) and always first
DefFileInfo *main_objects_def = new DefFileInfo(this, C4CFN_Objects, "");
children.emplace_back(main_objects_def);
bool has_default_objects_found = false;
for (auto & root_iter : ::Reloc)
{
const char *root = root_iter.strBuf.getData();
for (DirectoryIterator def_file_iter(root); *def_file_iter; ++def_file_iter)
{
const char *def_file = ::GetFilename(*def_file_iter);
if (WildcardMatch(C4CFN_DefFiles, def_file))
{
// Set path of main objects if found
if (!has_default_objects_found && !strcmp(C4CFN_Objects, def_file))
{
main_objects_def->root_path.Copy(root);
continue;
}
// Avoid duplicates on top level
bool dup = false;
for (auto & child : children)
if (!strcmp(child->GetName(), def_file))
{
dup = true; break;
}
if (dup) continue;
children.emplace_back(new DefFileInfo(this, def_file, root));
}
}
}
}
void C4ConsoleQtDefinitionFileListModel::DefFileInfo::SetSelected(bool to_val, bool forced)
{
if (forced)
force_selected = to_val;
else
user_selected = to_val;
// Selection propagates to children
for (auto & child : children)
{
child->SetSelected(to_val, forced);
}
}
bool C4ConsoleQtDefinitionFileListModel::DefFileInfo::OpenGroup()
{
children.clear();
was_opened = true; // mark as opened even if fails to prevent permanent re-loading of broken groups
if (parent->IsRoot())
{
if (!grp.Open((root_path + DirectorySeparator + filename).getData())) return false;
}
else
{
if (!grp.OpenAsChild(&parent->grp, filename.getData())) return false;
}
// Init child array (without loading full groups)
StdStrBuf child_filename;
children.reserve(grp.EntryCount(C4CFN_DefFiles));
grp.ResetSearch();
while (grp.FindNextEntry(C4CFN_DefFiles, &child_filename))
children.emplace_back(new DefFileInfo(this, child_filename.getData(), nullptr));
return true;
}
int32_t C4ConsoleQtDefinitionFileListModel::DefFileInfo::GetChildCount()
{
if (!was_opened) OpenGroup();
return children.size();
}
C4ConsoleQtDefinitionFileListModel::DefFileInfo *C4ConsoleQtDefinitionFileListModel::DefFileInfo::GetChild(int32_t index)
{
if (!was_opened) OpenGroup();
if (index >= children.size()) return nullptr;
return children[index].get();
}
int32_t C4ConsoleQtDefinitionFileListModel::DefFileInfo::GetChildIndex(const DefFileInfo *child)
{
auto iter = std::find_if(children.begin(), children.end(),
[child](std::unique_ptr<DefFileInfo> & item)->bool { return item.get() == child; });
if (iter == children.end()) return -1; // not found
return int32_t(iter - children.begin());
}
void C4ConsoleQtDefinitionFileListModel::DefFileInfo::AddUserSelectedDefinitions(std::list<const char *> *result) const
{
// Add parent-most selected
// Ignore any forced selection even if also selected by user.
// It may have been selected first and then forced by the scenario preset after the template has been switched)
if (!IsForceSelected())
{
if (IsUserSelected())
result->push_back(full_filename.getData());
else
for (auto &iter : children) iter->AddUserSelectedDefinitions(result);
}
}
void C4ConsoleQtDefinitionFileListModel::DefFileInfo::AddSelectedDefinitions(std::list<const char *> *result) const
{
// Add parent-most selected
if (IsSelected())
result->push_back(full_filename.getData());
else
for (auto &iter : children) iter->AddSelectedDefinitions(result);
}
void C4ConsoleQtDefinitionFileListModel::DefFileInfo::SetForcedSelection(const char *selected_def_filepath)
{
// Filenames are assumed to be case insensitive for the Windows client
if (SEqualNoCase(selected_def_filepath, full_filename.getData()))
{
// This is the def to be force-selected
SetSelected(true, true);
}
else if (is_root || (SEqual2NoCase(selected_def_filepath, full_filename.getData()) && selected_def_filepath[full_filename.getLength()] == '\\'))
{
// One of the child definitions should be force-selected
if (!was_opened) OpenGroup();
for (auto &iter : children) iter->SetForcedSelection(selected_def_filepath);
}
}
void C4ConsoleQtDefinitionFileListModel::DefFileInfo::AddExtraDef(const char *def)
{
assert(is_root);
// Ignore if it was already added
// Could also avoid adding child definitions if they are already in the list.
// E.g. do not add both foo.ocs\bar.ocd and foo.ocs\bar.ocd\baz.ocd, but keep only the parent path.
// But it's overkill for a case that will probably never happen and would pose just a minor nuisance if it does.
for (auto &iter : children)
{
if (SEqualNoCase(iter->full_filename.getData(), def))
{
return;
}
}
// Add using user path as root (extra defs will always be in the user path because they are not used by our main system templates)
children.emplace_back(new DefFileInfo(this, def, ::Config.General.UserDataPath));
}
C4ConsoleQtDefinitionFileListModel::C4ConsoleQtDefinitionFileListModel() = default;
C4ConsoleQtDefinitionFileListModel::~C4ConsoleQtDefinitionFileListModel() = default;
void C4ConsoleQtDefinitionFileListModel::AddExtraDef(const char *def)
{
root.AddExtraDef(def);
}
std::list<const char *> C4ConsoleQtDefinitionFileListModel::GetUserSelectedDefinitions() const
{
std::list<const char *> result;
root.AddUserSelectedDefinitions(&result);
return result;
}
std::list<const char *> C4ConsoleQtDefinitionFileListModel::GetSelectedDefinitions() const
{
std::list<const char *> result;
root.AddSelectedDefinitions(&result);
return result;
}
void C4ConsoleQtDefinitionFileListModel::SetForcedSelection(const std::list<const char *> &defs)
{
beginResetModel();
// Unselect previous
root.SetSelected(false, true);
// Force new selection
for (const char *def : defs)
{
root.SetForcedSelection(def);
}
endResetModel();
}
int C4ConsoleQtDefinitionFileListModel::rowCount(const QModelIndex & parent) const
{
if (!parent.isValid()) return root.GetChildCount();
DefFileInfo *parent_def = static_cast<DefFileInfo *>(parent.internalPointer());
if (!parent_def) return 0;
return parent_def->GetChildCount();
}
int C4ConsoleQtDefinitionFileListModel::columnCount(const QModelIndex & parent) const
{
return 1; // Name
}
QVariant C4ConsoleQtDefinitionFileListModel::data(const QModelIndex & index, int role) const
{
DefFileInfo *def = static_cast<DefFileInfo *>(index.internalPointer());
if (!def) return QVariant();
// Query latest data from prop list
if (role == Qt::DisplayRole)
{
return QString(def->GetName());
}
else if (role == Qt::CheckStateRole)
{
return def->IsSelected() ? Qt::Checked : Qt::Unchecked;
}
// Nothing to show
return QVariant();
}
QModelIndex C4ConsoleQtDefinitionFileListModel::index(int row, int column, const QModelIndex &parent) const
{
if (column) return QModelIndex();
DefFileInfo *parent_def = &root;
if (parent.isValid()) parent_def = static_cast<DefFileInfo *>(parent.internalPointer());
if (!parent_def) return QModelIndex();
return createIndex(row, column, parent_def->GetChild(row));
}
QModelIndex C4ConsoleQtDefinitionFileListModel::parent(const QModelIndex &index) const
{
DefFileInfo *def = static_cast<DefFileInfo *>(index.internalPointer());
if (!def) return QModelIndex();
DefFileInfo *parent_def = def->GetParent();
if (!parent_def) return QModelIndex();
int32_t def_index = parent_def->GetChildIndex(def);
if (def_index < 0) return QModelIndex(); // can't happen
return createIndex(def_index, 0, parent_def);
}
Qt::ItemFlags C4ConsoleQtDefinitionFileListModel::flags(const QModelIndex &index) const
{
Qt::ItemFlags flags = Qt::ItemIsSelectable | Qt::ItemIsUserCheckable;
DefFileInfo *def = static_cast<DefFileInfo *>(index.internalPointer());
if (def && !def->IsDisabled()) flags |= Qt::ItemIsEnabled;
return flags;
}
bool C4ConsoleQtDefinitionFileListModel::setData(const QModelIndex& index, const QVariant& value, int role)
{
// Adjust check-state
if (role == Qt::CheckStateRole)
{
DefFileInfo *def = static_cast<DefFileInfo *>(index.internalPointer());
if (def && !def->IsDisabled())
{
def->SetSelected(value.toBool(), false);
// Update changed index and all children
int32_t child_count = def->GetChildCount();
QModelIndex end_changed = index;
if (child_count) end_changed = createIndex(child_count - 1, 0, def->GetChild(child_count - 1));
emit dataChanged(index, end_changed);
}
}
return true;
}
/* New scenario dialogue */
C4ConsoleQtNewScenarioDlg::C4ConsoleQtNewScenarioDlg(class QMainWindow *parent_window)
: QDialog(parent_window, Qt::WindowSystemMenuHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint)
, has_custom_filename(false)
{
ui.setupUi(this);
adjustSize();
setMinimumSize(size());
// Create scenario at user path by default
ui.filenameEdit->setText(::Config.General.UserDataPath);
// Fill definition file model
QItemSelectionModel *m = ui.definitionTreeView->selectionModel();
ui.definitionTreeView->setModel(&def_file_model);
delete m;
// Init scenario template list
InitScenarioTemplateList();
}
void C4ConsoleQtNewScenarioDlg::InitScenarioTemplateList()
{
// Init template scenarios from user and system folder
// Clear previous
ui.templateComboBox->clear();
C4Group system_templates, user_templates;
system_templates.OpenAsChild(&::Application.SystemGroup, C4CFN_Template);
user_templates.Open(Config.AtUserDataPath(C4CFN_Template));
for (C4Group *template_group : { &system_templates, &user_templates })
{
if (template_group->IsOpen()) // open may have failed (e.g. if it doesn't exist)
{
// All scenarios within the template group are possible scenario templates
template_group->ResetSearch();
StdStrBuf template_filename;
while (template_group->FindNextEntry(C4CFN_ScenarioFiles, &template_filename))
{
bool is_default = (template_group == &system_templates) && (template_filename == C4CFN_DefaultScenarioTemplate);
AddScenarioTemplate(*template_group, template_filename.getData(), is_default);
}
}
}
// TODO could sort elements. But should usually be sorted within the packed groups anyway.
}
void C4ConsoleQtNewScenarioDlg::AddScenarioTemplate(C4Group &parent, const char *filename, bool is_default)
{
// Load scenario information from group and add as template
C4Group grp;
if (!grp.OpenAsChild(&parent, filename)) return;
C4Scenario template_c4s;
if (!template_c4s.Load(grp)) return;
// Title from file or scenario core
C4ComponentHost title_file;
StdCopyStrBuf title(template_c4s.Head.Title);
C4Language::LoadComponentHost(&title_file, grp, C4CFN_Title, Config.General.LanguageEx);
title_file.GetLanguageString(Config.General.LanguageEx, title);
// Add it; remember full path as user data
StdStrBuf template_path(grp.GetFullName());
ui.templateComboBox->addItem(QString(title.getData()), QString(template_path.getData()));
all_template_c4s.push_back(template_c4s);
// Add any extra definition (e.g. pointing into a scenario) to selection model
// "extra" definitions are those that use non-ocd-files anywhere in their path
auto c4s_defs = template_c4s.Definitions.GetModulesAsList();
for (const char *c4s_def : c4s_defs)
{
char c4s_def_component[_MAX_PATH + 1];
int32_t i = 0;
bool is_extra_def = false;
while (SCopySegment(c4s_def, i++, c4s_def_component, '\\', _MAX_PATH))
{
if (!WildcardMatch(C4CFN_DefFiles, c4s_def_component))
{
is_extra_def = true;
break;
}
}
if (is_extra_def)
{
def_file_model.AddExtraDef(c4s_def);
}
}
// Default selection
if (is_default) ui.templateComboBox->setCurrentIndex(ui.templateComboBox->count()-1);
}
bool C4ConsoleQtNewScenarioDlg::IsHostAsNetwork() const
{
return ui.startInNetworkCheckbox->isChecked();
}
void C4ConsoleQtNewScenarioDlg::SelectedTemplateChanged(int new_selection)
{
// Update forced definition selection for template
if (new_selection >= 0 && new_selection < all_template_c4s.size())
{
const C4Scenario &template_c4s = all_template_c4s[new_selection];
def_file_model.SetForcedSelection(template_c4s.Definitions.GetModulesAsList());
}
else
{
def_file_model.SetForcedSelection(std::list<const char *>());
}
}
bool C4ConsoleQtNewScenarioDlg::CreateScenario()
{
// Try to create scenario from template. Unpack if necessery.
QVariant tmpl_data = ui.templateComboBox->currentData();
Log(tmpl_data.toString().toUtf8());
StdStrBuf template_filename;
template_filename.Copy(tmpl_data.toString().toUtf8());
if (DirectoryExists(template_filename.getData()))
{
if (!CopyDirectory(template_filename.getData(), filename.getData(), true))
{
return false;
}
}
else
{
if (!C4Group_CopyItem(template_filename.getData(), filename.getData(), true, true))
{
return false;
}
if (!C4Group_UnpackDirectory(filename.getData()))
{
return false;
}
}
C4Group grp;
if (!grp.Open(filename.getData()))
{
return false;
}
// Remove localized title file to ensure it's loaded from the scenario core
grp.DeleteEntry(C4CFN_WriteTitle);
// Update scenario core with settings from dialogue
C4Scenario c4s;
if (!c4s.Load(grp)) return false;
// Take over settings
c4s.Landscape.MapWdt.SetConstant(ui.mapWidthSpinBox->value());
c4s.Landscape.MapHgt.SetConstant(ui.mapHeightSpinBox->value());
c4s.Landscape.MapZoom.SetConstant(ui.mapZoomSpinBox->value());
c4s.Head.Title = ui.titleEdit->text().toStdString();
c4s.Game.Mode.Copy(ui.gameModeComboBox->currentText().toUtf8());
if (c4s.Game.Mode == "Undefined") c4s.Game.Mode.Clear();
filename.Copy(ui.filenameEdit->text().toUtf8());
std::list<const char *> definitions = def_file_model.GetUserSelectedDefinitions();
StdStrBuf forced_definitions;
c4s.Definitions.GetModules(&forced_definitions);
const char *forced_definitions_c = forced_definitions.getData();
std::ostringstream definitions_join(forced_definitions_c ? forced_definitions_c : nullptr, std::ostringstream::ate);
if (definitions.size())
{
// definitions_join = definitions.join(";")
if (forced_definitions.getLength())
{
// Combine both forced and user-selected definitions
definitions_join << ";";
}
auto iter_end = definitions.end();
std::copy(definitions.begin(), --iter_end, std::ostream_iterator<std::string>(definitions_join, ";"));
definitions_join << *iter_end;
}
c4s.Definitions.SetModules(definitions_join.str().c_str());
if (!c4s.Save(grp))
{
return false;
}
// Group saving not needed because it's unpacked.
//if (!grp.Save()) return false;
return true;
}
void C4ConsoleQtNewScenarioDlg::CreatePressed()
{
// Check validity of settings
if (!ui.titleEdit->text().length())
{
DoError(::LoadResStr("IDS_ERR_ENTERTITLE"));
ui.titleEdit->setFocus();
return;
}
if (ItemExists(filename.getData()))
{
DoError(::LoadResStr("IDS_ERR_NEWSCENARIOFILEEXISTS"));
ui.titleEdit->setFocus();
return;
}
std::list<const char *> definitions = def_file_model.GetSelectedDefinitions();
if (definitions.size() > C4S_MaxDefinitions)
{
DoError(FormatString(::LoadResStr("IDS_ERR_TOOMANYDEFINITIONS"), (int)definitions.size(), (int)C4S_MaxDefinitions).getData());
ui.definitionTreeView->setFocus();
return;
}
if (!CreateScenario())
{
EraseItem(filename.getData());
DoError(::LoadResStr("IDS_ERR_CREATESCENARIO"));
ui.titleEdit->setFocus();
return;
}
// Close dialogue with OK
accept();
}
// Filter for allowed characters in filename
// (Also replace space, because spaces in filenames suk)
static char ReplaceSpecialFilenameChars(char c)
{
const char *special_chars = R"(\/:<>|$?" )";
return strchr(special_chars, c) ? '_' : c;
}
void C4ConsoleQtNewScenarioDlg::TitleChanged(const QString &new_title)
{
if (!has_custom_filename)
{
// Default filename by title
std::string filename = new_title.toStdString();
std::transform(filename.begin(), filename.end(), filename.begin(), ReplaceSpecialFilenameChars);
filename += (C4CFN_ScenarioFiles+1);
const char *filename_full = Config.AtUserDataPath(filename.c_str());
ui.filenameEdit->setText(filename_full);
this->filename.Copy(filename_full);
}
}
void C4ConsoleQtNewScenarioDlg::DoError(const char *msg)
{
QMessageBox::critical(this, ::LoadResStr("IDS_ERR_TITLE"), QString(msg));
}
void C4ConsoleQtNewScenarioDlg::BrowsePressed()
{
// Browse for new filename to be used instead of the filename generated from the title
QString new_file;
for (;;)
{
new_file = QFileDialog::getSaveFileName(this, LoadResStr("IDS_CNS_NEWSCENARIO"), Config.General.UserDataPath, QString("%1 (%2)").arg(LoadResStr("IDS_CNS_SCENARIOFILE")).arg(C4CFN_ScenarioFiles), nullptr, QFileDialog::DontConfirmOverwrite);
if (!new_file.size()) return;
// Extension must be .ocs
if (!new_file.endsWith(C4CFN_ScenarioFiles + 1)) new_file += (C4CFN_ScenarioFiles + 1);
if (!ItemExists(new_file.toUtf8())) break;
// Overwriting of existing scenarios not supported
QMessageBox::critical(this, ::LoadResStr("IDS_ERR_TITLE"), ::LoadResStr("IDS_ERR_NEWSCENARIOFILEEXISTS"));
}
filename.Copy(new_file.toUtf8());
ui.filenameEdit->setText(filename.getData()); // set from converted filename just in case weird stuff happened in toUtf8
// After setting a new filename, it no longer changes when changing the title
has_custom_filename = true;
}