/* * 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 & 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 *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 *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 C4ConsoleQtDefinitionFileListModel::GetUserSelectedDefinitions() const { std::list result; root.AddUserSelectedDefinitions(&result); return result; } std::list C4ConsoleQtDefinitionFileListModel::GetSelectedDefinitions() const { std::list result; root.AddSelectedDefinitions(&result); return result; } void C4ConsoleQtDefinitionFileListModel::SetForcedSelection(const std::list &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(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(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(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(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(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(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()); } } 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 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(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 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; }