#include "editor.h" #include "draggablepixmapitem.h" #include "imageproviders.h" #include "log.h" #include "mapconnection.h" #include "currentselectedmetatilespixmapitem.h" #include "mapsceneeventfilter.h" #include "metatile.h" #include "montabwidget.h" #include "encountertablemodel.h" #include "editcommands.h" #include "config.h" #include "scripting.h" #include "customattributestable.h" #include #include #include #include #include #include static bool selectNewEvents = false; // 2D array mapping collision+elevation combos to an icon. QList> Editor::collisionIcons; Editor::Editor(Ui::MainWindow* ui) { this->ui = ui; this->selected_events = new QList; this->settings = new Settings(); this->playerViewRect = new MovableRect(&this->settings->playerViewRectEnabled, 30 * 8, 20 * 8, qRgb(255, 255, 255)); this->cursorMapTileRect = new CursorTileRect(&this->settings->cursorTileRectEnabled, qRgb(255, 255, 255)); this->map_ruler = new MapRuler(4); connect(this->map_ruler, &MapRuler::statusChanged, this, &Editor::mapRulerStatusChanged); /// Instead of updating the selected events after every single undo action /// (eg when the user rolls back several at once), only reselect events when /// the index is changed. connect(&editGroup, &QUndoGroup::indexChanged, [this](int) { if (selectNewEvents) { updateSelectedEvents(); selectNewEvents = false; } }); } Editor::~Editor() { delete this->selected_events; delete this->settings; delete this->playerViewRect; delete this->cursorMapTileRect; delete this->map_ruler; for (auto sublist : collisionIcons) qDeleteAll(sublist); closeProject(); } void Editor::saveProject() { if (project) { saveUiFields(); project->saveAllMaps(); project->saveAllDataStructures(); } } void Editor::save() { if (project && map) { saveUiFields(); project->saveMap(map); project->saveAllDataStructures(); } } void Editor::saveUiFields() { saveEncounterTabData(); } void Editor::closeProject() { if (!this->project) return; Scripting::cb_ProjectClosed(this->project->root); delete this->project; } void Editor::setEditingMap() { current_view = map_item; if (map_item) { map_item->paintingMode = MapPixmapItem::PaintMode::Metatiles; displayMapConnections(); map_item->draw(); map_item->setVisible(true); } if (collision_item) { collision_item->setVisible(false); } if (events_group) { events_group->setVisible(false); } setBorderItemsVisible(ui->checkBox_ToggleBorder->isChecked()); setConnectionItemsVisible(ui->checkBox_ToggleBorder->isChecked()); setConnectionsEditable(false); this->cursorMapTileRect->stopSingleTileMode(); this->cursorMapTileRect->setActive(true); setMapEditingButtonsEnabled(true); } void Editor::setEditingCollision() { current_view = collision_item; if (collision_item) { displayMapConnections(); collision_item->draw(); collision_item->setVisible(true); } if (map_item) { map_item->paintingMode = MapPixmapItem::PaintMode::Metatiles; map_item->draw(); map_item->setVisible(true); } if (events_group) { events_group->setVisible(false); } setBorderItemsVisible(ui->checkBox_ToggleBorder->isChecked()); setConnectionItemsVisible(ui->checkBox_ToggleBorder->isChecked()); setConnectionsEditable(false); this->cursorMapTileRect->setSingleTileMode(); this->cursorMapTileRect->setActive(true); setMapEditingButtonsEnabled(true); } void Editor::setEditingObjects() { current_view = map_item; if (events_group) { events_group->setVisible(true); } if (map_item) { map_item->paintingMode = MapPixmapItem::PaintMode::EventObjects; displayMapConnections(); map_item->draw(); map_item->setVisible(true); } if (collision_item) { collision_item->setVisible(false); } setBorderItemsVisible(ui->checkBox_ToggleBorder->isChecked()); setConnectionItemsVisible(ui->checkBox_ToggleBorder->isChecked()); setConnectionsEditable(false); this->cursorMapTileRect->setSingleTileMode(); this->cursorMapTileRect->setActive(false); updateWarpEventWarnings(); setMapEditingButtonsEnabled(false); } void Editor::setMapEditingButtonsEnabled(bool enabled) { this->ui->toolButton_Fill->setEnabled(enabled); this->ui->toolButton_Dropper->setEnabled(enabled); this->ui->pushButton_ChangeDimensions->setEnabled(enabled); // If the fill button is pressed, unpress it and select the pointer. if (!enabled && (this->ui->toolButton_Fill->isChecked() || this->ui->toolButton_Dropper->isChecked())) { this->map_edit_mode = "select"; this->settings->mapCursor = QCursor(); this->cursorMapTileRect->setSingleTileMode(); this->ui->toolButton_Fill->setChecked(false); this->ui->toolButton_Dropper->setChecked(false); this->ui->toolButton_Select->setChecked(true); } this->ui->checkBox_smartPaths->setEnabled(enabled); } void Editor::setEditingConnections() { current_view = map_item; if (map_item) { map_item->paintingMode = MapPixmapItem::PaintMode::Disabled; map_item->draw(); map_item->setVisible(true); populateConnectionMapPickers(); ui->label_NumConnections->setText(QString::number(map->connections.length())); setDiveEmergeControls(); bool controlsEnabled = selected_connection_item != nullptr; setConnectionEditControlsEnabled(controlsEnabled); if (selected_connection_item) { onConnectionOffsetChanged(selected_connection_item->connection->offset); setConnectionMap(selected_connection_item->connection->map_name); setCurrentConnectionDirection(selected_connection_item->connection->direction); } maskNonVisibleConnectionTiles(); } if (collision_item) { collision_item->setVisible(false); } if (events_group) { events_group->setVisible(false); } setBorderItemsVisible(true, 0.4); setConnectionItemsVisible(true); setConnectionsEditable(true); this->cursorMapTileRect->setSingleTileMode(); this->cursorMapTileRect->setActive(false); } void Editor::displayWildMonTables() { QStackedWidget *stack = ui->stackedWidget_WildMons; QComboBox *labelCombo = ui->comboBox_EncounterGroupLabel; // delete widgets from previous map data if they exist while (stack->count()) { QWidget *oldWidget = stack->widget(0); stack->removeWidget(oldWidget); delete oldWidget; } labelCombo->clear(); // Don't try to read encounter data if it doesn't exist on disk for this map. if (!project->wildMonData.contains(map->constantName)) { return; } for (auto groupPair : project->wildMonData[map->constantName]) labelCombo->addItem(groupPair.first); labelCombo->setCurrentText(labelCombo->itemText(0)); int labelIndex = 0; for (auto labelPair : project->wildMonData[map->constantName]) { QString label = labelPair.first; WildPokemonHeader header = project->wildMonData[map->constantName][label]; MonTabWidget *tabWidget = new MonTabWidget(this); stack->insertWidget(labelIndex++, tabWidget); int tabIndex = 0; for (EncounterField monField : project->wildMonFields) { QString fieldName = monField.name; tabWidget->clearTableAt(tabIndex); if (project->wildMonData.contains(map->constantName) && header.wildMons[fieldName].active) { tabWidget->populateTab(tabIndex, header.wildMons[fieldName]); } else { tabWidget->setTabActive(tabIndex, false); } tabIndex++; } } stack->setCurrentIndex(0); } void Editor::addNewWildMonGroup(QWidget *window) { QStackedWidget *stack = ui->stackedWidget_WildMons; QComboBox *labelCombo = ui->comboBox_EncounterGroupLabel; int stackIndex = stack->currentIndex(); QDialog dialog(window, Qt::WindowTitleHint | Qt::WindowCloseButtonHint); dialog.setWindowTitle("New Wild Encounter Group Label"); dialog.setWindowModality(Qt::NonModal); QFormLayout form(&dialog); QDialogButtonBox buttonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &dialog); QLineEdit *lineEdit = new QLineEdit(); lineEdit->setClearButtonEnabled(true); form.addRow(new QLabel("Group Base Label:"), lineEdit); static const QRegularExpression re_validChars("[_A-Za-z0-9]*"); QRegularExpressionValidator *validator = new QRegularExpressionValidator(re_validChars); lineEdit->setValidator(validator); connect(lineEdit, &QLineEdit::textChanged, [this, &lineEdit, &buttonBox](QString text){ if (this->project->encounterGroupLabels.contains(text)) { lineEdit->setStyleSheet("QLineEdit { background-color: rgba(255, 0, 0, 25%) }"); buttonBox.button(QDialogButtonBox::Ok)->setDisabled(true); } else { lineEdit->setStyleSheet(""); buttonBox.button(QDialogButtonBox::Ok)->setEnabled(true); } }); // Give a default value to the label. lineEdit->setText(QString("g%1%2").arg(map->name).arg(stack->count())); // Fields [x] copy from existing QLabel *fieldsLabel = new QLabel("Fields:"); form.addRow(fieldsLabel); QCheckBox *copyCheckbox = new QCheckBox; copyCheckbox->setEnabled(stack->count()); form.addRow(new QLabel("Copy from current group"), copyCheckbox); QVector fieldCheckboxes; for (EncounterField monField : project->wildMonFields) { QCheckBox *fieldCheckbox = new QCheckBox; fieldCheckboxes.append(fieldCheckbox); form.addRow(new QLabel(monField.name), fieldCheckbox); } // Reading from ui here so not saving to disk before user. connect(copyCheckbox, &QCheckBox::stateChanged, [=](int state){ if (state == Qt::Checked) { int fieldIndex = 0; MonTabWidget *monWidget = static_cast(stack->widget(stack->currentIndex())); for (EncounterField monField : project->wildMonFields) { fieldCheckboxes[fieldIndex]->setChecked(monWidget->isTabEnabled(fieldIndex)); fieldCheckboxes[fieldIndex]->setEnabled(false); fieldIndex++; } } else if (state == Qt::Unchecked) { int fieldIndex = 0; for (EncounterField monField : project->wildMonFields) { fieldCheckboxes[fieldIndex]->setEnabled(true); fieldIndex++; } } }); connect(&buttonBox, &QDialogButtonBox::accepted, [&dialog, &lineEdit, this](){ QString newLabel = lineEdit->text(); if (!newLabel.isEmpty()) { this->project->encounterGroupLabels.append(newLabel); dialog.accept(); } }); connect(&buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); form.addRow(&buttonBox); if (dialog.exec() == QDialog::Accepted) { WildPokemonHeader header; for (EncounterField& monField : project->wildMonFields) { QString fieldName = monField.name; header.wildMons[fieldName].active = false; header.wildMons[fieldName].encounterRate = 0; } MonTabWidget *tabWidget = new MonTabWidget(this); stack->insertWidget(stack->count(), tabWidget); labelCombo->addItem(lineEdit->text()); labelCombo->setCurrentIndex(labelCombo->count() - 1); int tabIndex = 0; for (EncounterField &monField : project->wildMonFields) { QString fieldName = monField.name; tabWidget->clearTableAt(tabIndex); if (fieldCheckboxes[tabIndex]->isChecked()) { if (copyCheckbox->isChecked()) { MonTabWidget *copyFrom = static_cast(stack->widget(stackIndex)); if (copyFrom->isTabEnabled(tabIndex)) { QTableView *monTable = copyFrom->tableAt(tabIndex); EncounterTableModel *model = static_cast(monTable->model()); header.wildMons[fieldName] = model->encounterData(); } else { header.wildMons[fieldName] = getDefaultMonInfo(monField); } } else { header.wildMons[fieldName] = getDefaultMonInfo(monField); } tabWidget->populateTab(tabIndex, header.wildMons[fieldName]); } else { tabWidget->setTabActive(tabIndex, false); } tabIndex++; } saveEncounterTabData(); emit wildMonDataChanged(); } } void Editor::deleteWildMonGroup() { QComboBox *labelCombo = ui->comboBox_EncounterGroupLabel; if (labelCombo->count() < 1) { return; } QMessageBox msgBox; msgBox.setText("Confirm Delete"); msgBox.setInformativeText("Are you sure you want to delete " + labelCombo->currentText() + "?"); QPushButton *deleteButton = msgBox.addButton("Delete", QMessageBox::DestructiveRole); msgBox.addButton(QMessageBox::Cancel); msgBox.setDefaultButton(QMessageBox::Cancel); msgBox.exec(); if (msgBox.clickedButton() == deleteButton) { auto it = project->wildMonData.find(map->constantName); if (it == project->wildMonData.end()) { logError(QString("Failed to find data for map %1. Unable to delete").arg(map->constantName)); return; } int i = project->encounterGroupLabels.indexOf(labelCombo->currentText()); if (i < 0) { logError(QString("Failed to find selected wild mon group: %1. Unable to delete") .arg(labelCombo->currentText())); return; } it.value().erase(labelCombo->currentText()); project->encounterGroupLabels.remove(i); displayWildMonTables(); emit wildMonDataChanged(); } } void Editor::configureEncounterJSON(QWidget *window) { QVector fieldSlots; EncounterFields tempFields = project->wildMonFields; QLabel *totalLabel = new QLabel; // lambda: Update the total displayed at the bottom of the Configure JSON // window. Take groups into account when applicable. auto updateTotal = [&fieldSlots, totalLabel](EncounterField ¤tField) { int total = 0, spinnerIndex = 0; QString groupTotalMessage; QMap groupTotals; for (auto keyPair : currentField.groups) { groupTotals.insert(keyPair.first, 0);// add to group map and initialize total to zero } for (auto slot : fieldSlots) { QSpinBox *spinner = slot->findChild(); int val = spinner->value(); currentField.encounterRates[spinnerIndex] = val; if (!currentField.groups.empty()) { for (auto keyPair : currentField.groups) { QString key = keyPair.first; if (currentField.groups[key].contains(spinnerIndex)) { groupTotals[key] += val; break; } } } else { total += val; } spinnerIndex++; } if (!currentField.groups.empty()) { groupTotalMessage += "Totals: "; for (auto keyPair : currentField.groups) { QString key = keyPair.first; groupTotalMessage += QString("%1 (%2),\t").arg(groupTotals[key]).arg(key); } groupTotalMessage.chop(2); } else { groupTotalMessage = QString("Total: %1").arg(QString::number(total)); } if (total > 0xFF) { totalLabel->setTextFormat(Qt::RichText); groupTotalMessage += QString("\tWARNING: value exceeds the limit for a u8 variable."); } totalLabel->setText(groupTotalMessage); }; // lambda: Create a new "slot", which is the widget containing a spinner and an index label. // Add the slot to a list of fieldSlots, which exists to keep track of them for memory management. auto createNewSlot = [&fieldSlots, &tempFields, &updateTotal](int index, EncounterField ¤tField) { QLabel *indexLabel = new QLabel(QString("Index: %1").arg(QString::number(index))); QSpinBox *chanceSpinner = new QSpinBox; int chance = currentField.encounterRates.at(index); chanceSpinner->setMinimum(1); chanceSpinner->setMaximum(9999); chanceSpinner->setValue(chance); connect(chanceSpinner, QOverload::of(&QSpinBox::valueChanged), [&updateTotal, ¤tField](int) { updateTotal(currentField); }); bool useGroups = !currentField.groups.empty(); QFrame *slotChoiceFrame = new QFrame; QVBoxLayout *slotChoiceLayout = new QVBoxLayout; if (useGroups) { QComboBox *groupCombo = new QComboBox; connect(groupCombo, QOverload::of(&QComboBox::textActivated), [&tempFields, ¤tField, &updateTotal, index](QString newGroupName) { for (EncounterField &field : tempFields) { if (field.name == currentField.name) { for (auto groupNameIterator : field.groups) { QString groupName = groupNameIterator.first; if (field.groups[groupName].contains(index)) { field.groups[groupName].removeAll(index); break; } } for (auto groupNameIterator : field.groups) { QString groupName = groupNameIterator.first; if (groupName == newGroupName) field.groups[newGroupName].append(index); } break; } } updateTotal(currentField); }); for (auto groupNameIterator : currentField.groups) { groupCombo->addItem(groupNameIterator.first); } QString currentGroupName; for (auto groupNameIterator : currentField.groups) { QString groupName = groupNameIterator.first; if (currentField.groups[groupName].contains(index)) { currentGroupName = groupName; break; } } groupCombo->setCurrentText(currentGroupName); slotChoiceLayout->addWidget(groupCombo); } slotChoiceLayout->addWidget(chanceSpinner); slotChoiceFrame->setLayout(slotChoiceLayout); QFrame *slot = new QFrame; QHBoxLayout *slotLayout = new QHBoxLayout; slotLayout->addWidget(indexLabel); slotLayout->addWidget(slotChoiceFrame); slot->setLayout(slotLayout); fieldSlots.append(slot); return slot; }; QDialog dialog(window, Qt::WindowTitleHint | Qt::WindowCloseButtonHint); dialog.setWindowTitle("Configure Wild Encounter Fields"); dialog.setWindowModality(Qt::NonModal); QGridLayout grid; QDialogButtonBox buttonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &dialog); connect(&buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(&buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); // lambda: Get a QStringList of the existing field names. auto getFieldNames = [&tempFields]() { QStringList fieldNames; for (EncounterField field : tempFields) fieldNames.append(field.name); return fieldNames; }; // lambda: Draws the slot widgets onto a grid (4 wide) on the dialog window. auto drawSlotWidgets = [&dialog, &grid, &createNewSlot, &fieldSlots, &updateTotal, &tempFields](int index) { // Clear them first. while (!fieldSlots.isEmpty()) { auto slot = fieldSlots.takeFirst(); grid.removeWidget(slot); delete slot; } if (!tempFields.size()) { return; } if (index >= tempFields.size()) { index = tempFields.size() - 1; } EncounterField ¤tField = tempFields[index]; for (int i = 0; i < currentField.encounterRates.size(); i++) { grid.addWidget(createNewSlot(i, currentField), i / 4 + 1, i % 4); } updateTotal(currentField); dialog.adjustSize();// TODO: why is this updating only on second call? reproduce: land->fishing->rock_smash->water }; QComboBox *fieldChoices = new QComboBox; connect(fieldChoices, QOverload::of(&QComboBox::currentIndexChanged), drawSlotWidgets); fieldChoices->addItems(getFieldNames()); QLabel *fieldChoiceLabel = new QLabel("Field"); // Button to create new fields in the JSON. QPushButton *addFieldButton = new QPushButton("Add New Field..."); connect(addFieldButton, &QPushButton::clicked, [fieldChoices, &tempFields]() { QDialog newNameDialog(nullptr, Qt::WindowTitleHint | Qt::WindowCloseButtonHint); newNameDialog.setWindowModality(Qt::NonModal); QDialogButtonBox newFieldButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &newNameDialog); connect(&newFieldButtonBox, &QDialogButtonBox::accepted, &newNameDialog, &QDialog::accept); connect(&newFieldButtonBox, &QDialogButtonBox::rejected, &newNameDialog, &QDialog::reject); QLineEdit *newNameEdit = new QLineEdit; newNameEdit->setClearButtonEnabled(true); QFormLayout newFieldForm(&newNameDialog); newFieldForm.addRow("Field Name", newNameEdit); newFieldForm.addRow(&newFieldButtonBox); if (newNameDialog.exec() == QDialog::Accepted) { QString newFieldName = newNameEdit->text(); QVector newFieldRates(1, 100); tempFields.append({newFieldName, newFieldRates, {}}); fieldChoices->addItem(newFieldName); fieldChoices->setCurrentIndex(fieldChoices->count() - 1); } }); QPushButton *deleteFieldButton = new QPushButton("Delete Field"); connect(deleteFieldButton, &QPushButton::clicked, [drawSlotWidgets, fieldChoices, &tempFields]() { if (tempFields.size() < 2) return;// don't delete last int index = fieldChoices->currentIndex(); fieldChoices->removeItem(index); tempFields.remove(index); drawSlotWidgets(index); }); QPushButton *addSlotButton = new QPushButton(QIcon(":/icons/add.ico"), ""); addSlotButton->setFlat(true); connect(addSlotButton, &QPushButton::clicked, [&fieldChoices, &drawSlotWidgets, &tempFields]() { EncounterField &field = tempFields[fieldChoices->currentIndex()]; field.encounterRates.append(1); drawSlotWidgets(fieldChoices->currentIndex()); }); QPushButton *removeSlotButton = new QPushButton(QIcon(":/icons/delete.ico"), ""); removeSlotButton->setFlat(true); connect(removeSlotButton, &QPushButton::clicked, [&fieldChoices, &drawSlotWidgets, &tempFields]() { EncounterField &field = tempFields[fieldChoices->currentIndex()]; int lastIndex = field.encounterRates.size() - 1; if (lastIndex > 0) field.encounterRates.removeLast(); for (auto &g : field.groups) { field.groups[g.first].removeAll(lastIndex); } drawSlotWidgets(fieldChoices->currentIndex()); }); // TODO: method for editing groups? QFrame firstRow; QHBoxLayout firstRowLayout; firstRowLayout.addWidget(fieldChoiceLabel); firstRowLayout.addWidget(fieldChoices); firstRowLayout.addWidget(deleteFieldButton); firstRowLayout.addWidget(addFieldButton); firstRowLayout.addWidget(removeSlotButton); firstRowLayout.addWidget(addSlotButton); firstRow.setLayout(&firstRowLayout); grid.addWidget(&firstRow, 0, 0, 1, 4, Qt::AlignLeft); QHBoxLayout lastRow; lastRow.addWidget(totalLabel); lastRow.addWidget(&buttonBox); // To keep the total and button box at the bottom of the window. QVBoxLayout layout(&dialog); QFrame *frameTop = new QFrame; frameTop->setLayout(&grid); layout.addWidget(frameTop); QFrame *frameBottom = new QFrame; frameBottom->setLayout(&lastRow); layout.addWidget(frameBottom); if (dialog.exec() == QDialog::Accepted) { updateEncounterFields(tempFields); // Re-draw the tab accordingly. displayWildMonTables(); emit wildMonDataChanged(); } } void Editor::saveEncounterTabData() { // This function does not save to disk so it is safe to use before user clicks Save. QStackedWidget *stack = ui->stackedWidget_WildMons; QComboBox *labelCombo = ui->comboBox_EncounterGroupLabel; if (!stack->count()) return; tsl::ordered_map &encounterMap = project->wildMonData[map->constantName]; for (int groupIndex = 0; groupIndex < stack->count(); groupIndex++) { MonTabWidget *tabWidget = static_cast(stack->widget(groupIndex)); WildPokemonHeader &encounterHeader = encounterMap[labelCombo->itemText(groupIndex)]; int fieldIndex = 0; for (EncounterField monField : project->wildMonFields) { QString fieldName = monField.name; if (!tabWidget->isTabEnabled(fieldIndex++)) { encounterHeader.wildMons.erase(fieldName); continue; } QTableView *monTable = tabWidget->tableAt(fieldIndex - 1); EncounterTableModel *model = static_cast(monTable->model()); encounterHeader.wildMons[fieldName] = model->encounterData(); } } } void Editor::updateEncounterFields(EncounterFields newFields) { EncounterFields oldFields = project->wildMonFields; // Go through fields and determine whether we need to update a field. // If the field is new, do nothing. // If the field is deleted, remove from all maps. // If the field is changed, change all maps accordingly. for (EncounterField oldField : oldFields) { QString oldFieldName = oldField.name; bool fieldDeleted = true; for (EncounterField newField : newFields) { QString newFieldName = newField.name; if (oldFieldName == newFieldName) { fieldDeleted = false; if (oldField.encounterRates.size() != newField.encounterRates.size()) { for (auto mapPair : project->wildMonData) { QString map = mapPair.first; for (auto groupNamePair : project->wildMonData[map]) { QString groupName = groupNamePair.first; WildPokemonHeader &monHeader = project->wildMonData[map][groupName]; for (auto fieldNamePair : monHeader.wildMons) { QString fieldName = fieldNamePair.first; if (fieldName == oldFieldName) { monHeader.wildMons[fieldName].wildPokemon.resize(newField.encounterRates.size()); } } } } } } } if (fieldDeleted) { for (auto mapPair : project->wildMonData) { QString map = mapPair.first; for (auto groupNamePair : project->wildMonData[map]) { QString groupName = groupNamePair.first; WildPokemonHeader &monHeader = project->wildMonData[map][groupName]; for (auto fieldNamePair : monHeader.wildMons) { QString fieldName = fieldNamePair.first; if (fieldName == oldFieldName) { monHeader.wildMons.erase(fieldName); } } } } } } project->wildMonFields = newFields; } void Editor::setDiveEmergeControls() { ui->comboBox_DiveMap->blockSignals(true); ui->comboBox_EmergeMap->blockSignals(true); ui->comboBox_DiveMap->setCurrentText(""); ui->comboBox_EmergeMap->setCurrentText(""); for (MapConnection* connection : map->connections) { if (connection->direction == "dive") { ui->comboBox_DiveMap->setCurrentText(connection->map_name); } else if (connection->direction == "emerge") { ui->comboBox_EmergeMap->setCurrentText(connection->map_name); } } ui->comboBox_DiveMap->blockSignals(false); ui->comboBox_EmergeMap->blockSignals(false); } void Editor::populateConnectionMapPickers() { ui->comboBox_ConnectedMap->blockSignals(true); ui->comboBox_DiveMap->blockSignals(true); ui->comboBox_EmergeMap->blockSignals(true); ui->comboBox_ConnectedMap->clear(); ui->comboBox_ConnectedMap->addItems(project->mapNames); ui->comboBox_DiveMap->clear(); ui->comboBox_DiveMap->addItems(project->mapNames); ui->comboBox_EmergeMap->clear(); ui->comboBox_EmergeMap->addItems(project->mapNames); ui->comboBox_ConnectedMap->blockSignals(false); ui->comboBox_DiveMap->blockSignals(true); ui->comboBox_EmergeMap->blockSignals(true); } void Editor::setConnectionItemsVisible(bool visible) { for (ConnectionPixmapItem* item : connection_items) { item->setVisible(visible); item->setEnabled(visible); } } void Editor::setBorderItemsVisible(bool visible, qreal opacity) { for (QGraphicsPixmapItem* item : borderItems) { item->setVisible(visible); item->setOpacity(opacity); } } void Editor::setCurrentConnectionDirection(QString curDirection) { if (!selected_connection_item) return; Map *connected_map = project->getMap(selected_connection_item->connection->map_name); if (!connected_map) { return; } selected_connection_item->connection->direction = curDirection; QPixmap pixmap = connected_map->renderConnection(*selected_connection_item->connection, map->layout); int offset = selected_connection_item->connection->offset; selected_connection_item->initialOffset = offset; int x = 0, y = 0; if (selected_connection_item->connection->direction == "up") { x = offset * 16; y = -pixmap.height(); } else if (selected_connection_item->connection->direction == "down") { x = offset * 16; y = map->getHeight() * 16; } else if (selected_connection_item->connection->direction == "left") { x = -pixmap.width(); y = offset * 16; } else if (selected_connection_item->connection->direction == "right") { x = map->getWidth() * 16; y = offset * 16; } selected_connection_item->basePixmap = pixmap; QPainter painter(&pixmap); painter.setPen(QColor(255, 0, 255)); painter.drawRect(0, 0, pixmap.width() - 1, pixmap.height() - 1); painter.end(); selected_connection_item->setPixmap(pixmap); selected_connection_item->initialX = x; selected_connection_item->initialY = y; selected_connection_item->blockSignals(true); selected_connection_item->setX(x); selected_connection_item->setY(y); selected_connection_item->setZValue(-1); selected_connection_item->blockSignals(false); setConnectionEditControlValues(selected_connection_item->connection); } void Editor::updateCurrentConnectionDirection(QString curDirection) { if (!selected_connection_item) return; QString originalDirection = selected_connection_item->connection->direction; setCurrentConnectionDirection(curDirection); updateMirroredConnectionDirection(selected_connection_item->connection, originalDirection); maskNonVisibleConnectionTiles(); } void Editor::onConnectionMoved(MapConnection* connection) { updateMirroredConnectionOffset(connection); onConnectionOffsetChanged(connection->offset); maskNonVisibleConnectionTiles(); } void Editor::onConnectionOffsetChanged(int newOffset) { ui->spinBox_ConnectionOffset->blockSignals(true); ui->spinBox_ConnectionOffset->setValue(newOffset); ui->spinBox_ConnectionOffset->blockSignals(false); } void Editor::setConnectionEditControlValues(MapConnection* connection) { QString mapName = connection ? connection->map_name : ""; QString direction = connection ? connection->direction : ""; int offset = connection ? connection->offset : 0; ui->comboBox_ConnectedMap->blockSignals(true); ui->comboBox_ConnectionDirection->blockSignals(true); ui->spinBox_ConnectionOffset->blockSignals(true); ui->comboBox_ConnectedMap->setCurrentText(mapName); ui->comboBox_ConnectionDirection->setCurrentText(direction); ui->spinBox_ConnectionOffset->setValue(offset); ui->comboBox_ConnectedMap->blockSignals(false); ui->comboBox_ConnectionDirection->blockSignals(false); ui->spinBox_ConnectionOffset->blockSignals(false); } void Editor::setConnectionEditControlsEnabled(bool enabled) { ui->comboBox_ConnectionDirection->setEnabled(enabled); ui->comboBox_ConnectedMap->setEnabled(enabled); ui->spinBox_ConnectionOffset->setEnabled(enabled); if (!enabled) { setConnectionEditControlValues(nullptr); } } void Editor::setConnectionsEditable(bool editable) { for (ConnectionPixmapItem* item : connection_items) { item->setEditable(editable); item->updateHighlight(item == selected_connection_item); } } void Editor::onConnectionItemSelected(ConnectionPixmapItem* connectionItem) { if (!connectionItem) return; selected_connection_item = connectionItem; for (ConnectionPixmapItem* item : connection_items) item->updateHighlight(item == selected_connection_item); setConnectionEditControlsEnabled(true); setConnectionEditControlValues(selected_connection_item->connection); ui->spinBox_ConnectionOffset->setMaximum(selected_connection_item->getMaxOffset()); ui->spinBox_ConnectionOffset->setMinimum(selected_connection_item->getMinOffset()); onConnectionOffsetChanged(selected_connection_item->connection->offset); } void Editor::setSelectedConnectionFromMap(QString mapName) { // Search for the first connection that connects to the given map map. for (ConnectionPixmapItem* item : connection_items) { if (item->connection->map_name == mapName) { onConnectionItemSelected(item); break; } } } void Editor::onConnectionItemDoubleClicked(ConnectionPixmapItem* connectionItem) { emit loadMapRequested(connectionItem->connection->map_name, map->name); } void Editor::onConnectionDirectionChanged(QString newDirection) { ui->comboBox_ConnectionDirection->blockSignals(true); ui->comboBox_ConnectionDirection->setCurrentText(newDirection); ui->comboBox_ConnectionDirection->blockSignals(false); } void Editor::onBorderMetatilesChanged() { displayMapBorder(); setBorderItemsVisible(ui->checkBox_ToggleBorder->isChecked()); } void Editor::onHoveredMovementPermissionChanged(uint16_t collision, uint16_t elevation) { this->ui->statusBar->showMessage(this->getMovementPermissionText(collision, elevation)); } void Editor::onHoveredMovementPermissionCleared() { this->ui->statusBar->clearMessage(); } QString Editor::getMetatileDisplayMessage(uint16_t metatileId) { Metatile *metatile = Tileset::getMetatile(metatileId, map->layout->tileset_primary, map->layout->tileset_secondary); QString label = Tileset::getMetatileLabel(metatileId, map->layout->tileset_primary, map->layout->tileset_secondary); QString message = QString("Metatile: %1").arg(Metatile::getMetatileIdString(metatileId)); if (label.size()) message += QString(" \"%1\"").arg(label); if (metatile && metatile->behavior() != 0) { // Skip MB_NORMAL const QString behaviorStr = this->project->metatileBehaviorMapInverse.value(metatile->behavior(), "0x" + QString::number(metatile->behavior(), 16)); message += QString(", Behavior: %1").arg(behaviorStr); } return message; } void Editor::onHoveredMetatileSelectionChanged(uint16_t metatileId) { this->ui->statusBar->showMessage(getMetatileDisplayMessage(metatileId)); } void Editor::onHoveredMetatileSelectionCleared() { this->ui->statusBar->clearMessage(); } void Editor::onSelectedMetatilesChanged() { QPoint size = this->metatile_selector_item->getSelectionDimensions(); this->cursorMapTileRect->updateSelectionSize(size.x(), size.y()); this->redrawCurrentMetatilesSelection(); } void Editor::onWheelZoom(int s) { // Don't zoom the map when the user accidentally scrolls while performing a magic fill. (ctrl + middle button click) if (!(QApplication::mouseButtons() & Qt::MiddleButton)) { scaleMapView(s); } } const QList zoomLevels = QList { 0.5, 0.75, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0, }; void Editor::scaleMapView(int s) { // Clamp the scale index to a valid value. int nextScaleIndex = this->scaleIndex + s; if (nextScaleIndex < 0) nextScaleIndex = 0; if (nextScaleIndex >= zoomLevels.size()) nextScaleIndex = zoomLevels.size() - 1; // Early exit if the scale index hasn't changed. if (nextScaleIndex == this->scaleIndex) return; // Set the graphics views' scale transformation based // on the new scale amount. this->scaleIndex = nextScaleIndex; double scaleFactor = zoomLevels[nextScaleIndex]; QTransform transform = QTransform::fromScale(scaleFactor, scaleFactor); ui->graphicsView_Map->setTransform(transform); ui->graphicsView_Connections->setTransform(transform); } void Editor::updateCursorRectPos(int x, int y) { if (this->playerViewRect) this->playerViewRect->updateLocation(x, y); if (this->cursorMapTileRect) this->cursorMapTileRect->updateLocation(x, y); if (ui->graphicsView_Map->scene()) ui->graphicsView_Map->scene()->update(); } void Editor::setCursorRectVisible(bool visible) { if (this->playerViewRect) this->playerViewRect->setVisible(visible); if (this->cursorMapTileRect) this->cursorMapTileRect->setVisible(visible); if (ui->graphicsView_Map->scene()) ui->graphicsView_Map->scene()->update(); } void Editor::onHoveredMapMetatileChanged(const QPoint &pos) { int x = pos.x(); int y = pos.y(); if (!map->isWithinBounds(x, y)) return; this->updateCursorRectPos(x, y); if (map_item->paintingMode == MapPixmapItem::PaintMode::Metatiles) { int blockIndex = y * map->getWidth() + x; int metatileId = map->layout->blockdata.at(blockIndex).metatileId(); this->ui->statusBar->showMessage(QString("X: %1, Y: %2, %3, Scale = %4x") .arg(x) .arg(y) .arg(getMetatileDisplayMessage(metatileId)) .arg(QString::number(zoomLevels[this->scaleIndex], 'g', 2))); } else if (map_item->paintingMode == MapPixmapItem::PaintMode::EventObjects) { this->ui->statusBar->showMessage(QString("X: %1, Y: %2, Scale = %3x") .arg(x) .arg(y) .arg(QString::number(zoomLevels[this->scaleIndex], 'g', 2))); } Scripting::cb_BlockHoverChanged(x, y); } void Editor::onHoveredMapMetatileCleared() { this->setCursorRectVisible(false); if (map_item->paintingMode == MapPixmapItem::PaintMode::Metatiles || map_item->paintingMode == MapPixmapItem::PaintMode::EventObjects) { this->ui->statusBar->clearMessage(); } Scripting::cb_BlockHoverCleared(); } void Editor::onHoveredMapMovementPermissionChanged(int x, int y) { if (!map->isWithinBounds(x, y)) return; this->updateCursorRectPos(x, y); if (map_item->paintingMode == MapPixmapItem::PaintMode::Metatiles) { int blockIndex = y * map->getWidth() + x; uint16_t collision = map->layout->blockdata.at(blockIndex).collision(); uint16_t elevation = map->layout->blockdata.at(blockIndex).elevation(); QString message = QString("X: %1, Y: %2, %3") .arg(x) .arg(y) .arg(this->getMovementPermissionText(collision, elevation)); this->ui->statusBar->showMessage(message); } Scripting::cb_BlockHoverChanged(x, y); } void Editor::onHoveredMapMovementPermissionCleared() { this->setCursorRectVisible(false); if (map_item->paintingMode == MapPixmapItem::PaintMode::Metatiles) { this->ui->statusBar->clearMessage(); } Scripting::cb_BlockHoverCleared(); } QString Editor::getMovementPermissionText(uint16_t collision, uint16_t elevation) { QString message; if (collision != 0) { message = QString("Collision: Impassable (%1), Elevation: %2").arg(collision).arg(elevation); } else if (elevation == 0) { message = "Collision: Transition between elevations"; } else if (elevation == 15) { message = "Collision: Multi-Level (Bridge)"; } else if (elevation == 1) { message = "Collision: Surf"; } else { message = QString("Collision: Passable, Elevation: %1").arg(elevation); } return message; } bool Editor::setMap(QString map_name) { if (map_name.isEmpty()) { return false; } // disconnect previous map's signals so they are not firing // multiple times if set again in the future if (map) { map->disconnect(this); } if (project) { Map *loadedMap = project->loadMap(map_name); if (!loadedMap) { return false; } map = loadedMap; editGroup.addStack(&map->editHistory); editGroup.setActiveStack(&map->editHistory); selected_events->clear(); if (!displayMap()) { return false; } map_ruler->setMapDimensions(QSize(map->getWidth(), map->getHeight())); connect(map, &Map::mapDimensionsChanged, map_ruler, &MapRuler::setMapDimensions); connect(map, &Map::openScriptRequested, this, &Editor::openScript); updateSelectedEvents(); } return true; } void Editor::onMapStartPaint(QGraphicsSceneMouseEvent *event, MapPixmapItem *item) { if (item->paintingMode != MapPixmapItem::PaintMode::Metatiles) { return; } QPoint pos = Metatile::coordFromPixmapCoord(event->pos()); if (event->buttons() & Qt::RightButton && (map_edit_mode == "paint" || map_edit_mode == "fill")) { this->cursorMapTileRect->initRightClickSelectionAnchor(pos.x(), pos.y()); } else { this->cursorMapTileRect->initAnchor(pos.x(), pos.y()); } } void Editor::onMapEndPaint(QGraphicsSceneMouseEvent *, MapPixmapItem *item) { if (!(item->paintingMode == MapPixmapItem::PaintMode::Metatiles)) { return; } this->cursorMapTileRect->stopRightClickSelectionAnchor(); this->cursorMapTileRect->stopAnchor(); } void Editor::setSmartPathCursorMode(QGraphicsSceneMouseEvent *event) { bool shiftPressed = event->modifiers() & Qt::ShiftModifier; if (settings->smartPathsEnabled) { if (!shiftPressed) { this->cursorMapTileRect->setSmartPathMode(true); } else { this->cursorMapTileRect->setSmartPathMode(false); } } else { if (shiftPressed) { this->cursorMapTileRect->setSmartPathMode(true); } else { this->cursorMapTileRect->setSmartPathMode(false); } } } void Editor::setStraightPathCursorMode(QGraphicsSceneMouseEvent *event) { if (event->modifiers() & Qt::ControlModifier) { this->cursorMapTileRect->setStraightPathMode(true); } else { this->cursorMapTileRect->setStraightPathMode(false); } } void Editor::mouseEvent_map(QGraphicsSceneMouseEvent *event, MapPixmapItem *item) { // TODO: add event tab object painting tool buttons stuff here if (item->paintingMode == MapPixmapItem::PaintMode::Disabled) { return; } QPoint pos = Metatile::coordFromPixmapCoord(event->pos()); if (item->paintingMode == MapPixmapItem::PaintMode::Metatiles) { if (map_edit_mode == "paint") { if (event->buttons() & Qt::RightButton) { item->updateMetatileSelection(event); } else if (event->buttons() & Qt::MiddleButton) { if (event->modifiers() & Qt::ControlModifier) { item->magicFill(event); } else { item->floodFill(event); } } else { if (event->type() == QEvent::GraphicsSceneMouseRelease) { // Update the tile rectangle at the end of a click-drag selection this->updateCursorRectPos(pos.x(), pos.y()); } this->setSmartPathCursorMode(event); this->setStraightPathCursorMode(event); if (this->cursorMapTileRect->getStraightPathMode()) { item->lockNondominantAxis(event); pos = item->adjustCoords(pos); } item->paint(event); } } else if (map_edit_mode == "select") { item->select(event); } else if (map_edit_mode == "fill") { if (event->buttons() & Qt::RightButton) { item->updateMetatileSelection(event); } else if (event->modifiers() & Qt::ControlModifier) { item->magicFill(event); } else { item->floodFill(event); } } else if (map_edit_mode == "pick") { if (event->buttons() & Qt::RightButton) { item->updateMetatileSelection(event); } else { item->pick(event); } } else if (map_edit_mode == "shift") { this->setStraightPathCursorMode(event); if (this->cursorMapTileRect->getStraightPathMode()) { item->lockNondominantAxis(event); pos = item->adjustCoords(pos); } item->shift(event); } } else if (item->paintingMode == MapPixmapItem::PaintMode::EventObjects) { if (obj_edit_mode == "paint" && event->type() == QEvent::GraphicsSceneMousePress) { // Right-clicking while in paint mode will change mode to select. if (event->buttons() & Qt::RightButton) { this->obj_edit_mode = "select"; this->settings->mapCursor = QCursor(); this->cursorMapTileRect->setSingleTileMode(); this->ui->toolButton_Paint->setChecked(false); this->ui->toolButton_Select->setChecked(true); } else { // Left-clicking while in paint mode will add a new event of the // type of the first currently selected events. // Disallow adding heal locations, deleting them is not possible yet Event::Type eventType = Event::Type::Object; if (this->selected_events->size() > 0) eventType = this->selected_events->first()->event->getEventType(); if (eventType != Event::Type::HealLocation) { DraggablePixmapItem *newEvent = addNewEvent(eventType); if (newEvent) { newEvent->move(pos.x(), pos.y()); emit objectsChanged(); selectMapEvent(newEvent, false); } } } } else if (obj_edit_mode == "select") { // do nothing here, at least for now } else if (obj_edit_mode == "shift" && item->map) { static QPoint selection_origin; static unsigned actionId = 0; if (event->type() == QEvent::GraphicsSceneMouseRelease) { actionId++; } else { if (event->type() == QEvent::GraphicsSceneMousePress) { selection_origin = QPoint(pos.x(), pos.y()); } else if (event->type() == QEvent::GraphicsSceneMouseMove) { if (pos.x() != selection_origin.x() || pos.y() != selection_origin.y()) { int xDelta = pos.x() - selection_origin.x(); int yDelta = pos.y() - selection_origin.y(); QList selectedEvents; for (DraggablePixmapItem *item : getObjects()) { selectedEvents.append(item->event); } selection_origin = QPoint(pos.x(), pos.y()); map->editHistory.push(new EventShift(selectedEvents, xDelta, yDelta, actionId)); } } } } } } void Editor::mouseEvent_collision(QGraphicsSceneMouseEvent *event, CollisionPixmapItem *item) { if (item->paintingMode != MapPixmapItem::PaintMode::Metatiles) { return; } QPoint pos = Metatile::coordFromPixmapCoord(event->pos()); if (map_edit_mode == "paint") { if (event->buttons() & Qt::RightButton) { item->updateMovementPermissionSelection(event); } else if (event->buttons() & Qt::MiddleButton) { if (event->modifiers() & Qt::ControlModifier) { item->magicFill(event); } else { item->floodFill(event); } } else { this->setStraightPathCursorMode(event); if (this->cursorMapTileRect->getStraightPathMode()) { item->lockNondominantAxis(event); pos = item->adjustCoords(pos); } item->paint(event); } } else if (map_edit_mode == "select") { item->select(event); } else if (map_edit_mode == "fill") { if (event->buttons() & Qt::RightButton) { item->pick(event); } else if (event->modifiers() & Qt::ControlModifier) { item->magicFill(event); } else { item->floodFill(event); } } else if (map_edit_mode == "pick") { item->pick(event); } else if (map_edit_mode == "shift") { this->setStraightPathCursorMode(event); if (this->cursorMapTileRect->getStraightPathMode()) { item->lockNondominantAxis(event); pos = item->adjustCoords(pos); } item->shift(event); } } bool Editor::displayMap() { if (!scene) { scene = new QGraphicsScene; MapSceneEventFilter *filter = new MapSceneEventFilter(); scene->installEventFilter(filter); connect(filter, &MapSceneEventFilter::wheelZoom, this, &Editor::onWheelZoom); scene->installEventFilter(this->map_ruler); } if (map_item && scene) { scene->removeItem(map_item); delete map_item; scene->removeItem(this->map_ruler); } displayMetatileSelector(); displayMovementPermissionSelector(); displayMapMetatiles(); displayMapMovementPermissions(); displayBorderMetatiles(); displayCurrentMetatilesSelection(); displayMapEvents(); displayMapConnections(); displayMapBorder(); displayMapGrid(); displayWildMonTables(); this->map_ruler->setZValue(1000); scene->addItem(this->map_ruler); if (map_item) { map_item->setVisible(false); } if (collision_item) { collision_item->setVisible(false); } if (events_group) { events_group->setVisible(false); } return true; } void Editor::displayMetatileSelector() { if (metatile_selector_item && metatile_selector_item->scene()) { metatile_selector_item->scene()->removeItem(metatile_selector_item); delete scene_metatiles; } scene_metatiles = new QGraphicsScene; if (!metatile_selector_item) { metatile_selector_item = new MetatileSelector(8, map); connect(metatile_selector_item, &MetatileSelector::hoveredMetatileSelectionChanged, this, &Editor::onHoveredMetatileSelectionChanged); connect(metatile_selector_item, &MetatileSelector::hoveredMetatileSelectionCleared, this, &Editor::onHoveredMetatileSelectionCleared); connect(metatile_selector_item, &MetatileSelector::selectedMetatilesChanged, this, &Editor::onSelectedMetatilesChanged); metatile_selector_item->select(0); } else { metatile_selector_item->setMap(map); if (metatile_selector_item->primaryTileset && metatile_selector_item->primaryTileset != map->layout->tileset_primary) emit tilesetUpdated(map->layout->tileset_primary->name); if (metatile_selector_item->secondaryTileset && metatile_selector_item->secondaryTileset != map->layout->tileset_secondary) emit tilesetUpdated(map->layout->tileset_secondary->name); metatile_selector_item->setTilesets(map->layout->tileset_primary, map->layout->tileset_secondary); } scene_metatiles->addItem(metatile_selector_item); } void Editor::displayMapMetatiles() { map_item = new MapPixmapItem(map, this->metatile_selector_item, this->settings); connect(map_item, &MapPixmapItem::mouseEvent, this, &Editor::mouseEvent_map); connect(map_item, &MapPixmapItem::startPaint, this, &Editor::onMapStartPaint); connect(map_item, &MapPixmapItem::endPaint, this, &Editor::onMapEndPaint); connect(map_item, &MapPixmapItem::hoveredMapMetatileChanged, this, &Editor::onHoveredMapMetatileChanged); connect(map_item, &MapPixmapItem::hoveredMapMetatileCleared, this, &Editor::onHoveredMapMetatileCleared); map_item->draw(true); scene->addItem(map_item); int tw = 16; int th = 16; scene->setSceneRect( -BORDER_DISTANCE * tw, -BORDER_DISTANCE * th, map_item->pixmap().width() + BORDER_DISTANCE * 2 * tw, map_item->pixmap().height() + BORDER_DISTANCE * 2 * th ); } void Editor::displayMapMovementPermissions() { if (collision_item && scene) { scene->removeItem(collision_item); delete collision_item; } collision_item = new CollisionPixmapItem(map, ui->spinBox_SelectedCollision, ui->spinBox_SelectedElevation, this->metatile_selector_item, this->settings, &this->collisionOpacity); connect(collision_item, &CollisionPixmapItem::mouseEvent, this, &Editor::mouseEvent_collision); connect(collision_item, &CollisionPixmapItem::hoveredMapMovementPermissionChanged, this, &Editor::onHoveredMapMovementPermissionChanged); connect(collision_item, &CollisionPixmapItem::hoveredMapMovementPermissionCleared, this, &Editor::onHoveredMapMovementPermissionCleared); collision_item->draw(true); scene->addItem(collision_item); } void Editor::displayBorderMetatiles() { if (selected_border_metatiles_item && selected_border_metatiles_item->scene()) { selected_border_metatiles_item->scene()->removeItem(selected_border_metatiles_item); delete selected_border_metatiles_item; } scene_selected_border_metatiles = new QGraphicsScene; selected_border_metatiles_item = new BorderMetatilesPixmapItem(map, this->metatile_selector_item); selected_border_metatiles_item->draw(); scene_selected_border_metatiles->addItem(selected_border_metatiles_item); connect(selected_border_metatiles_item, &BorderMetatilesPixmapItem::hoveredBorderMetatileSelectionChanged, this, &Editor::onHoveredMetatileSelectionChanged); connect(selected_border_metatiles_item, &BorderMetatilesPixmapItem::hoveredBorderMetatileSelectionCleared, this, &Editor::onHoveredMetatileSelectionCleared); connect(selected_border_metatiles_item, &BorderMetatilesPixmapItem::borderMetatilesChanged, this, &Editor::onBorderMetatilesChanged); } void Editor::displayCurrentMetatilesSelection() { if (current_metatile_selection_item && current_metatile_selection_item->scene()) { current_metatile_selection_item->scene()->removeItem(current_metatile_selection_item); delete current_metatile_selection_item; } scene_current_metatile_selection = new QGraphicsScene; current_metatile_selection_item = new CurrentSelectedMetatilesPixmapItem(map, this->metatile_selector_item); current_metatile_selection_item->draw(); scene_current_metatile_selection->addItem(current_metatile_selection_item); } void Editor::redrawCurrentMetatilesSelection() { if (current_metatile_selection_item) { current_metatile_selection_item->setMap(map); current_metatile_selection_item->draw(); emit currentMetatilesSelectionChanged(); } } void Editor::displayMovementPermissionSelector() { if (movement_permissions_selector_item && movement_permissions_selector_item->scene()) { movement_permissions_selector_item->scene()->removeItem(movement_permissions_selector_item); delete scene_collision_metatiles; } scene_collision_metatiles = new QGraphicsScene; if (!movement_permissions_selector_item) { movement_permissions_selector_item = new MovementPermissionsSelector(this->collisionSheetPixmap); connect(movement_permissions_selector_item, &MovementPermissionsSelector::hoveredMovementPermissionChanged, this, &Editor::onHoveredMovementPermissionChanged); connect(movement_permissions_selector_item, &MovementPermissionsSelector::hoveredMovementPermissionCleared, this, &Editor::onHoveredMovementPermissionCleared); connect(movement_permissions_selector_item, &SelectablePixmapItem::selectionChanged, [this](int x, int y, int, int) { this->setCollisionTabSpinBoxes(x, y); }); movement_permissions_selector_item->select(projectConfig.defaultCollision, projectConfig.defaultElevation); } scene_collision_metatiles->addItem(movement_permissions_selector_item); } void Editor::displayMapEvents() { if (events_group) { for (QGraphicsItem *child : events_group->childItems()) { events_group->removeFromGroup(child); delete child; } if (events_group->scene()) { events_group->scene()->removeItem(events_group); } delete events_group; } selected_events->clear(); events_group = new QGraphicsItemGroup; scene->addItem(events_group); QList events = map->getAllEvents(); for (Event *event : events) { project->setEventPixmap(event); addMapEvent(event); } //objects_group->setFiltersChildEvents(false); events_group->setHandlesChildEvents(false); } DraggablePixmapItem *Editor::addMapEvent(Event *event) { DraggablePixmapItem *object = new DraggablePixmapItem(event, this); this->redrawObject(object); events_group->addToGroup(object); return object; } void Editor::displayMapConnections() { for (ConnectionPixmapItem* item : connection_items) { if (item->scene()) { item->scene()->removeItem(item); } delete item; } selected_connection_item = nullptr; connection_items.clear(); for (MapConnection *connection : map->connections) { if (connection->direction == "dive" || connection->direction == "emerge") { continue; } createConnectionItem(connection); } if (!connection_items.empty()) { onConnectionItemSelected(connection_items.first()); } maskNonVisibleConnectionTiles(); } void Editor::createConnectionItem(MapConnection* connection) { Map *connected_map = project->getMap(connection->map_name); if (!connected_map) { return; } QPixmap pixmap = connected_map->renderConnection(*connection, map->layout); int offset = connection->offset; int x = 0, y = 0; if (connection->direction == "up") { x = offset * 16; y = -pixmap.height(); } else if (connection->direction == "down") { x = offset * 16; y = map->getHeight() * 16; } else if (connection->direction == "left") { x = -pixmap.width(); y = offset * 16; } else if (connection->direction == "right") { x = map->getWidth() * 16; y = offset * 16; } ConnectionPixmapItem *item = new ConnectionPixmapItem(pixmap, connection, x, y, map->getWidth(), map->getHeight()); item->setX(x); item->setY(y); item->setZValue(-1); scene->addItem(item); connect(item, &ConnectionPixmapItem::connectionMoved, this, &Editor::onConnectionMoved); connect(item, &ConnectionPixmapItem::connectionItemSelected, this, &Editor::onConnectionItemSelected); connect(item, &ConnectionPixmapItem::connectionItemDoubleClicked, this, &Editor::onConnectionItemDoubleClicked); connection_items.append(item); } // Hides connected map tiles that cannot be seen from the current map (beyond BORDER_DISTANCE). void Editor::maskNonVisibleConnectionTiles() { if (connection_mask) { if (connection_mask->scene()) { connection_mask->scene()->removeItem(connection_mask); } delete connection_mask; connection_mask = nullptr; } QPainterPath mask; mask.addRect(scene->itemsBoundingRect().toRect()); mask.addRect( -BORDER_DISTANCE * 16, -BORDER_DISTANCE * 16, (map->getWidth() + BORDER_DISTANCE * 2) * 16, (map->getHeight() + BORDER_DISTANCE * 2) * 16 ); // Mask the tiles with the current theme's background color. QPen pen(ui->graphicsView_Map->palette().color(QPalette::Active, QPalette::Base)); QBrush brush(ui->graphicsView_Map->palette().color(QPalette::Active, QPalette::Base)); connection_mask = scene->addPath(mask, pen, brush); } void Editor::displayMapBorder() { for (QGraphicsPixmapItem* item : borderItems) { if (item->scene()) { item->scene()->removeItem(item); } delete item; } borderItems.clear(); int borderWidth = map->getBorderWidth(); int borderHeight = map->getBorderHeight(); int borderHorzDist = getBorderDrawDistance(borderWidth); int borderVertDist = getBorderDrawDistance(borderHeight); QPixmap pixmap = map->renderBorder(); for (int y = -borderVertDist; y < map->getHeight() + borderVertDist; y += borderHeight) for (int x = -borderHorzDist; x < map->getWidth() + borderHorzDist; x += borderWidth) { QGraphicsPixmapItem *item = new QGraphicsPixmapItem(pixmap); item->setX(x * 16); item->setY(y * 16); item->setZValue(-3); scene->addItem(item); borderItems.append(item); } } void Editor::updateMapBorder() { QPixmap pixmap = this->map->renderBorder(true); for (auto item : this->borderItems) { item->setPixmap(pixmap); } } void Editor::updateMapConnections() { for (int i = 0; i < connection_items.size(); i++) { Map *connected_map = project->getMap(connection_items[i]->connection->map_name); if (!connected_map) continue; QPixmap pixmap = connected_map->renderConnection(*(connection_items[i]->connection), map->layout); connection_items[i]->basePixmap = pixmap; connection_items[i]->setPixmap(pixmap); } maskNonVisibleConnectionTiles(); } int Editor::getBorderDrawDistance(int dimension) { // Draw sufficient border blocks to fill the player's view (BORDER_DISTANCE) if (dimension >= BORDER_DISTANCE) { return dimension; } else if (dimension) { return dimension * (BORDER_DISTANCE / dimension + (BORDER_DISTANCE % dimension ? 1 : 0)); } else { return BORDER_DISTANCE; } } void Editor::onToggleGridClicked(bool checked) { porymapConfig.showGrid = checked; if (ui->graphicsView_Map->scene()) ui->graphicsView_Map->scene()->update(); } void Editor::displayMapGrid() { for (QGraphicsLineItem* item : gridLines) { if (item) delete item; } gridLines.clear(); ui->checkBox_ToggleGrid->disconnect(); int pixelWidth = map->getWidth() * 16; int pixelHeight = map->getHeight() * 16; for (int i = 0; i <= map->getWidth(); i++) { int x = i * 16; QGraphicsLineItem *line = new QGraphicsLineItem(x, 0, x, pixelHeight); line->setVisible(ui->checkBox_ToggleGrid->isChecked()); gridLines.append(line); connect(ui->checkBox_ToggleGrid, &QCheckBox::toggled, [=](bool checked){line->setVisible(checked);}); } for (int j = 0; j <= map->getHeight(); j++) { int y = j * 16; QGraphicsLineItem *line = new QGraphicsLineItem(0, y, pixelWidth, y); line->setVisible(ui->checkBox_ToggleGrid->isChecked()); gridLines.append(line); connect(ui->checkBox_ToggleGrid, &QCheckBox::toggled, [=](bool checked){line->setVisible(checked);}); } connect(ui->checkBox_ToggleGrid, &QCheckBox::toggled, this, &Editor::onToggleGridClicked); } void Editor::updateConnectionOffset(int offset) { if (!selected_connection_item) return; selected_connection_item->blockSignals(true); offset = qMin(offset, selected_connection_item->getMaxOffset()); offset = qMax(offset, selected_connection_item->getMinOffset()); selected_connection_item->connection->offset = offset; if (selected_connection_item->connection->direction == "up" || selected_connection_item->connection->direction == "down") { selected_connection_item->setX(selected_connection_item->initialX + (offset - selected_connection_item->initialOffset) * 16); } else if (selected_connection_item->connection->direction == "left" || selected_connection_item->connection->direction == "right") { selected_connection_item->setY(selected_connection_item->initialY + (offset - selected_connection_item->initialOffset) * 16); } selected_connection_item->blockSignals(false); updateMirroredConnectionOffset(selected_connection_item->connection); maskNonVisibleConnectionTiles(); } void Editor::setConnectionMap(QString mapName) { if (!mapName.isEmpty() && !project->mapNames.contains(mapName)) { logError(QString("Invalid map name '%1' specified for connection.").arg(mapName)); return; } if (!selected_connection_item) return; if (mapName.isEmpty() || mapName == DYNAMIC_MAP_NAME) { removeCurrentConnection(); return; } QString originalMapName = selected_connection_item->connection->map_name; setConnectionEditControlsEnabled(true); selected_connection_item->connection->map_name = mapName; setCurrentConnectionDirection(selected_connection_item->connection->direction); // New map may have a different minimum offset than the last one. The maximum will be the same. int min = selected_connection_item->getMinOffset(); ui->spinBox_ConnectionOffset->setMinimum(min); onConnectionOffsetChanged(qMax(min, selected_connection_item->connection->offset)); updateMirroredConnectionMap(selected_connection_item->connection, originalMapName); maskNonVisibleConnectionTiles(); } void Editor::addNewConnection() { // Find direction with least number of connections. QMap directionCounts = QMap({{"up", 0}, {"right", 0}, {"down", 0}, {"left", 0}}); for (MapConnection* connection : map->connections) { directionCounts[connection->direction]++; } QString minDirection = "up"; int minCount = INT_MAX; for (QString direction : directionCounts.keys()) { if (directionCounts[direction] < minCount) { minDirection = direction; minCount = directionCounts[direction]; } } // Don't connect the map to itself. QString defaultMapName = project->mapNames.first(); if (defaultMapName == map->name) { defaultMapName = project->mapNames.value(1); } MapConnection* newConnection = new MapConnection; newConnection->direction = minDirection; newConnection->offset = 0; newConnection->map_name = defaultMapName; map->connections.append(newConnection); createConnectionItem(newConnection); onConnectionItemSelected(connection_items.last()); ui->label_NumConnections->setText(QString::number(map->connections.length())); updateMirroredConnection(newConnection, newConnection->direction, newConnection->map_name); } void Editor::updateMirroredConnectionOffset(MapConnection* connection) { updateMirroredConnection(connection, connection->direction, connection->map_name); } void Editor::updateMirroredConnectionDirection(MapConnection* connection, QString originalDirection) { updateMirroredConnection(connection, originalDirection, connection->map_name); } void Editor::updateMirroredConnectionMap(MapConnection* connection, QString originalMapName) { updateMirroredConnection(connection, connection->direction, originalMapName); } void Editor::removeMirroredConnection(MapConnection* connection) { updateMirroredConnection(connection, connection->direction, connection->map_name, true); } void Editor::updateMirroredConnection(MapConnection* connection, QString originalDirection, QString originalMapName, bool isDelete) { if (!ui->checkBox_MirrorConnections->isChecked()) return; Map* otherMap = project->getMap(originalMapName); if (!otherMap) return; static QMap oppositeDirections = QMap({ {"up", "down"}, {"right", "left"}, {"down", "up"}, {"left", "right"}, {"dive", "emerge"},{"emerge", "dive"}}); QString oppositeDirection = oppositeDirections.value(originalDirection); // Find the matching connection in the connected map. MapConnection* mirrorConnection = nullptr; for (MapConnection* conn : otherMap->connections) { if (conn->direction == oppositeDirection && conn->map_name == map->name) { mirrorConnection = conn; } } if (isDelete) { if (mirrorConnection) { otherMap->connections.removeOne(mirrorConnection); delete mirrorConnection; } return; } if (connection->direction != originalDirection || connection->map_name != originalMapName) { if (mirrorConnection) { otherMap->connections.removeOne(mirrorConnection); delete mirrorConnection; mirrorConnection = nullptr; otherMap = project->getMap(connection->map_name); } } // Create a new mirrored connection, if a matching one doesn't already exist. if (!mirrorConnection) { mirrorConnection = new MapConnection; mirrorConnection->direction = oppositeDirections.value(connection->direction); mirrorConnection->map_name = map->name; otherMap->connections.append(mirrorConnection); } mirrorConnection->offset = -connection->offset; } void Editor::removeCurrentConnection() { if (!selected_connection_item) return; map->connections.removeOne(selected_connection_item->connection); connection_items.removeOne(selected_connection_item); removeMirroredConnection(selected_connection_item->connection); if (selected_connection_item && selected_connection_item->scene()) { selected_connection_item->scene()->removeItem(selected_connection_item); delete selected_connection_item; } selected_connection_item = nullptr; setConnectionEditControlsEnabled(false); ui->spinBox_ConnectionOffset->setValue(0); ui->label_NumConnections->setText(QString::number(map->connections.length())); if (connection_items.length() > 0) { onConnectionItemSelected(connection_items.last()); } } void Editor::updateDiveMap(QString mapName) { updateDiveEmergeMap(mapName, "dive"); } void Editor::updateEmergeMap(QString mapName) { updateDiveEmergeMap(mapName, "emerge"); } void Editor::updateDiveEmergeMap(QString mapName, QString direction) { if (!mapName.isEmpty() && !project->mapNamesToMapConstants.contains(mapName)) { logError(QString("Invalid %1 connection map name: '%2'").arg(direction).arg(mapName)); return; } MapConnection* connection = nullptr; for (MapConnection* conn : map->connections) { if (conn->direction == direction) { connection = conn; break; } } if (mapName.isEmpty() || mapName == DYNAMIC_MAP_NAME) { // Remove dive/emerge connection if (connection) { map->connections.removeOne(connection); removeMirroredConnection(connection); } } else { if (!connection) { connection = new MapConnection; connection->direction = direction; connection->offset = 0; connection->map_name = mapName; map->connections.append(connection); updateMirroredConnection(connection, connection->direction, connection->map_name); } else { QString originalMapName = connection->map_name; connection->map_name = mapName; updateMirroredConnectionMap(connection, originalMapName); } } ui->label_NumConnections->setText(QString::number(map->connections.length())); } void Editor::updatePrimaryTileset(QString tilesetLabel, bool forceLoad) { if (map->layout->tileset_primary_label != tilesetLabel || forceLoad) { map->layout->tileset_primary_label = tilesetLabel; map->layout->tileset_primary = project->getTileset(tilesetLabel, forceLoad); map->clearBorderCache(); } } void Editor::updateSecondaryTileset(QString tilesetLabel, bool forceLoad) { if (map->layout->tileset_secondary_label != tilesetLabel || forceLoad) { map->layout->tileset_secondary_label = tilesetLabel; map->layout->tileset_secondary = project->getTileset(tilesetLabel, forceLoad); map->clearBorderCache(); } } void Editor::toggleBorderVisibility(bool visible, bool enableScriptCallback) { this->setBorderItemsVisible(visible); this->setConnectionItemsVisible(visible); porymapConfig.showBorder = visible; if (enableScriptCallback) Scripting::cb_BorderVisibilityToggled(visible); } void Editor::updateCustomMapHeaderValues(QTableWidget *table) { map->customHeaders = CustomAttributesTable::getAttributes(table); emit editedMapData(); } Tileset* Editor::getCurrentMapPrimaryTileset() { QString tilesetLabel = map->layout->tileset_primary_label; return project->getTileset(tilesetLabel); } QList Editor::getObjects() { QList list; for (QGraphicsItem *child : events_group->childItems()) { list.append(static_cast(child)); } return list; } void Editor::redrawObject(DraggablePixmapItem *item) { if (item && item->event && !item->event->getPixmap().isNull()) { qreal opacity = item->event->getUsingSprite() ? 1.0 : 0.7; item->setOpacity(opacity); project->setEventPixmap(item->event, true); item->setPixmap(item->event->getPixmap()); item->setShapeMode(QGraphicsPixmapItem::BoundingRectShape); if (selected_events && selected_events->contains(item)) { QImage image = item->pixmap().toImage(); QPainter painter(&image); painter.setPen(QColor(255, 0, 255)); painter.drawRect(0, 0, image.width() - 1, image.height() - 1); painter.end(); item->setPixmap(QPixmap::fromImage(image)); } item->updatePosition(); } } // Warp events display a warning if they're not positioned on a metatile with a warp behavior. void Editor::updateWarpEventWarning(Event *event) { if (porymapConfig.warpBehaviorWarningDisabled) return; if (!project || !map || !event || event->getEventType() != Event::Type::Warp) return; Block block; Metatile * metatile = nullptr; WarpEvent * warpEvent = static_cast(event); if (map->getBlock(warpEvent->getX(), warpEvent->getY(), &block)) { metatile = Tileset::getMetatile(block.metatileId(), map->layout->tileset_primary, map->layout->tileset_secondary); } // metatile may be null if the warp is in the map border. Display the warning in this case bool validWarpBehavior = metatile && projectConfig.warpBehaviors.contains(metatile->behavior()); warpEvent->setWarningEnabled(!validWarpBehavior); } // The warp event behavior warning is updated whenever the event moves or the event selection changes. // It does not respond to changes in the underlying metatile. To capture the common case of a user painting // metatiles on the Map tab then returning to the Events tab we update the warnings for all selected warp // events when the Events tab is opened. This does not cover the case where metatiles are painted while // still on the Events tab, such as by Undo/Redo or the scripting API. void Editor::updateWarpEventWarnings() { if (porymapConfig.warpBehaviorWarningDisabled) return; if (selected_events) { for (auto selection : *selected_events) updateWarpEventWarning(selection->event); } } void Editor::shouldReselectEvents() { selectNewEvents = true; } void Editor::updateSelectedEvents() { for (DraggablePixmapItem *item : getObjects()) { redrawObject(item); } emit objectsChanged(); } void Editor::selectMapEvent(DraggablePixmapItem *object) { selectMapEvent(object, false); } void Editor::selectMapEvent(DraggablePixmapItem *object, bool toggle) { if (selected_events && object) { if (selected_events->contains(object)) { if (toggle) { selected_events->removeOne(object); } } else { if (!toggle) { selected_events->clear(); } selected_events->append(object); } updateSelectedEvents(); } } void Editor::selectedEventIndexChanged(int index, Event::Group eventGroup) { int event_offs = Event::getIndexOffset(eventGroup); index = index - event_offs; Event *event = nullptr; if (index < this->map->events.value(eventGroup).length()) { event = this->map->events.value(eventGroup).at(index); } DraggablePixmapItem *selectedEvent = nullptr; for (QGraphicsItem *child : this->events_group->childItems()) { DraggablePixmapItem *item = static_cast(child); if (item->event == event) { selectedEvent = item; break; } } if (selectedEvent) { this->selectMapEvent(selectedEvent); } else { updateSelectedEvents(); } } void Editor::duplicateSelectedEvents() { if (!selected_events || !selected_events->length() || !map || !current_view || map_item->paintingMode != MapPixmapItem::PaintMode::EventObjects) return; QList selectedEvents; for (int i = 0; i < selected_events->length(); i++) { Event *original = selected_events->at(i)->event; Event::Type eventType = original->getEventType(); if (eventLimitReached(eventType)) { logWarn(QString("Skipping duplication, the map limit for events of type '%1' has been reached.").arg(Event::eventTypeToString(eventType))); continue; } if (eventType == Event::Type::HealLocation) { logWarn("Skipping duplication, event is a heal location."); continue; } Event *duplicate = original->duplicate(); if (!duplicate) { logError("Encountered a problem duplicating an event."); continue; } duplicate->setX(duplicate->getX() + 1); duplicate->setY(duplicate->getY() + 1); selectedEvents.append(duplicate); } map->editHistory.push(new EventDuplicate(this, map, selectedEvents)); } DraggablePixmapItem *Editor::addNewEvent(Event::Type type) { Event *event = nullptr; if (project && map && !eventLimitReached(type)) { switch (type) { case Event::Type::Object: event = new ObjectEvent(); break; case Event::Type::CloneObject: event = new CloneObjectEvent(); break; case Event::Type::Warp: event = new WarpEvent(); break; case Event::Type::Trigger: event = new TriggerEvent(); break; case Event::Type::WeatherTrigger: event = new WeatherTriggerEvent(); break; case Event::Type::Sign: event = new SignEvent(); break; case Event::Type::HiddenItem: event = new HiddenItemEvent(); break; case Event::Type::SecretBase: event = new SecretBaseEvent(); break; case Event::Type::HealLocation: { event = new HealLocationEvent(); event->setMap(this->map); event->setDefaultValues(this->project); HealLocation healLocation = HealLocation::fromEvent(event); project->healLocations.append(healLocation); ((HealLocationEvent *)event)->setIndex(project->healLocations.length()); break; } default: break; } if (!event) return nullptr; event->setMap(this->map); event->setDefaultValues(this->project); map->editHistory.push(new EventCreate(this, map, event)); return event->getPixmapItem(); } return nullptr; } // Currently only object events have an explicit limit bool Editor::eventLimitReached(Event::Type event_type) { if (project && map) { if (Event::typeToGroup(event_type) == Event::Group::Object) return map->events.value(Event::Group::Object).length() >= project->getMaxObjectEvents(); } return false; } void Editor::openMapScripts() const { openInTextEditor(map->getScriptsFilePath()); } void Editor::openScript(const QString &scriptLabel) const { // Find the location of scriptLabel. QStringList scriptPaths(map->getScriptsFilePath()); scriptPaths << project->getEventScriptsFilePaths(); int lineNum = 0; QString scriptPath = scriptPaths.first(); for (const auto &path : scriptPaths) { lineNum = ParseUtil::getScriptLineNumber(path, scriptLabel); if (lineNum != 0) { scriptPath = path; break; } } openInTextEditor(scriptPath, lineNum); } void Editor::openInTextEditor(const QString &path, int lineNum) { QString command = porymapConfig.textEditorGotoLine; if (command.isEmpty()) { // Open map scripts in the system's default editor. QDesktopServices::openUrl(QUrl::fromLocalFile(path)); } else { if (command.contains("%F")) { if (command.contains("%L")) command.replace("%L", QString::number(lineNum)); command.replace("%F", '\"' + path + '\"'); } else { command += " \"" + path + '\"'; } Editor::startDetachedProcess(command); } } void Editor::openProjectInTextEditor() const { QString command = porymapConfig.textEditorOpenFolder; if (command.contains("%D")) command.replace("%D", '\"' + project->root + '\"'); else command += " \"" + project->root + '\"'; startDetachedProcess(command); } bool Editor::startDetachedProcess(const QString &command, const QString &workingDirectory, qint64 *pid) { logInfo("Executing command: " + command); QProcess process; #ifdef Q_OS_WIN QStringList arguments = ParseUtil::splitShellCommand(command); const QString program = arguments.takeFirst(); QFileInfo programFileInfo(program); if (programFileInfo.isExecutable()) { process.setProgram(program); process.setArguments(arguments); } else { // program is a batch script (such as VSCode's 'code' script) and needs to be started by cmd.exe. process.setProgram(QProcessEnvironment::systemEnvironment().value("COMSPEC")); // Windows is finicky with quotes on the command-line. I can't explain why this difference is necessary. if (command.startsWith('"')) process.setNativeArguments("/c \"" + command + '"'); else process.setArguments(QStringList() << "/c" << program << arguments); } #else QStringList arguments = ParseUtil::splitShellCommand(command); process.setProgram(arguments.takeFirst()); process.setArguments(arguments); #endif process.setWorkingDirectory(workingDirectory); return process.startDetached(pid); } // It doesn't seem to be possible to prevent the mousePress event // from triggering both event's DraggablePixmapItem and the background mousePress. // Since the DraggablePixmapItem's event fires first, we can set a temp // variable "selectingEvent" so that we can detect whether or not the user // is clicking on the background instead of an event. void Editor::objectsView_onMousePress(QMouseEvent *event) { // make sure we are in object editing mode if (map_item && map_item->paintingMode != MapPixmapItem::PaintMode::EventObjects) { return; } if (this->obj_edit_mode == "paint" && event->buttons() & Qt::RightButton) { this->obj_edit_mode = "select"; this->settings->mapCursor = QCursor(); this->cursorMapTileRect->setSingleTileMode(); this->ui->toolButton_Paint->setChecked(false); this->ui->toolButton_Select->setChecked(true); } bool multiSelect = event->modifiers() & Qt::ControlModifier; if (!selectingEvent && !multiSelect && selected_events->length() > 1) { DraggablePixmapItem *first = selected_events->first(); selected_events->clear(); selected_events->append(first); updateSelectedEvents(); } selectingEvent = false; } void Editor::setCollisionTabSpinBoxes(uint16_t collision, uint16_t elevation) { const QSignalBlocker blocker1(ui->spinBox_SelectedCollision); const QSignalBlocker blocker2(ui->spinBox_SelectedElevation); ui->spinBox_SelectedCollision->setValue(collision); ui->spinBox_SelectedElevation->setValue(elevation); } // Custom collision graphics may be provided by the user. void Editor::setCollisionGraphics() { QString filepath = projectConfig.collisionSheetPath; QImage imgSheet; if (filepath.isEmpty()) { // No custom collision image specified, use the default. imgSheet = this->defaultCollisionImgSheet; } else { // Try to load custom collision image QString validPath = Project::getExistingFilepath(filepath); if (!validPath.isEmpty()) filepath = validPath; // Otherwise allow it to fail with the original path imgSheet = QImage(filepath); if (imgSheet.isNull()) { // Custom collision image failed to load, use default logWarn(QString("Failed to load custom collision image '%1', using default.").arg(filepath)); imgSheet = this->defaultCollisionImgSheet; } } // Users are not required to provide an image that gives an icon for every elevation/collision combination. // Instead they tell us how many are provided in their image by specifying the number of columns and rows. const int imgColumns = projectConfig.collisionSheetWidth; const int imgRows = projectConfig.collisionSheetHeight; // Create a pixmap for the selector on the Collision tab. If a project was previously opened we'll also need to refresh the selector. this->collisionSheetPixmap = QPixmap::fromImage(imgSheet).scaled(MovementPermissionsSelector::CellWidth * imgColumns, MovementPermissionsSelector::CellHeight * imgRows); if (this->movement_permissions_selector_item) this->movement_permissions_selector_item->setBasePixmap(this->collisionSheetPixmap); for (auto sublist : collisionIcons) qDeleteAll(sublist); collisionIcons.clear(); // Use the image sheet to create an icon for each collision/elevation combination. // Any icons for combinations that aren't provided by the image sheet are also created now using default graphics. const int w = 16, h = 16; imgSheet = imgSheet.scaled(w * imgColumns, h * imgRows); for (int collision = 0; collision <= Block::getMaxCollision(); collision++) { // If (collision >= imgColumns) here, it's a valid collision value, but it is not represented with an icon on the image sheet. // In this case we just use the rightmost collision icon. This is mostly to support the vanilla case, where technically 0-3 // are valid collision values, but 1-3 have the same meaning, so the vanilla collision selector image only has 2 columns. int x = ((collision < imgColumns) ? collision : (imgColumns - 1)) * w; QList sublist; for (int elevation = 0; elevation <= Block::getMaxElevation(); elevation++) { if (elevation < imgRows) { // This elevation has an icon on the image sheet, add it to the list int y = elevation * h; sublist.append(new QImage(imgSheet.copy(x, y, w, h))); } else { // This is a valid elevation value, but it has no icon on the image sheet. // Give it a placeholder "?" icon (red if impassable, white otherwise) sublist.append(new QImage(this->collisionPlaceholder.copy(x != 0 ? w : 0, 0, w, h))); } } collisionIcons.append(sublist); } }