/** * @brief Creates a new empty animation in this sprite. * * The index of the selection may change, since animations are sorted * alphabetically. * Emits rowsAboutToBeInserted(), adds the new animation * and then emits rowsInserted(), as required by QAbstractItemModel. * * Then, emits animation_created(). * * The newly created animation is not initially selected. * The existing selection is preserved, though the index of many * animations can change. * The selection is cleared before the operations and restored after, * updated with the new index. * * @param animation_name Name of the animation to create. * @throws EditorException in case of error. */ void SpriteModel::create_animation(const QString& animation_name) { // Make some checks first. if (animation_name.length() <= 0) { throw EditorException(tr("Animation name cannot be empty")); } if (animation_exists(animation_name)) { throw EditorException( tr("Animation '%1' already exists").arg(animation_name)); } // Save and clear the selection since a lot of indexes may change. Index selection = get_selected_index(); clear_selection(); // Add the animation to the sprite file. sprite.add_animation(animation_name.toStdString(), SpriteAnimationData()); // Rebuild indexes in the list model (indexes were shifted). build_index_map(); // Call beginInsertRows() as requested by QAbstractItemModel. int animation_nb = get_animation_nb(animation_name); beginInsertRows(QModelIndex(), animation_nb, animation_nb); // Update our animation model list. animations.insert(animation_nb, AnimationModel(animation_name)); // Notify people before restoring the selection, so that they have a // chance to know new indexes before receiving selection signals. endInsertRows(); emit animation_created(Index(animation_name)); // Restore the selection. set_selected_index(selection); }
/** * @brief Inserts a direction in an animation of this sprite. * @param index Index of the direction to insert. * @param data The direction data to insert. * @return Index of the inserted direction. * @throws EditorException in case of error. */ int SpriteModel::insert_direction( const Index& index, const SpriteAnimationDirectionData& data) { // Make some checks first. if (!animation_exists(index)) { throw EditorException( tr("Animation '%1' don't exists").arg(index.animation_name)); } // Save and clear the selection. Index selection = get_selected_index(); clear_selection(); // Insert the direction to the sprite file. SpriteAnimationData& animation_data = get_animation(index); animation_data.add_direction(data); int last_dir = animation_data.get_num_directions() - 1; animation_data.move_direction(last_dir, index.direction_nb); int direction_nb = std::min(index.direction_nb, last_dir); // Call beginInsertRows() as requested by QAbstractItemModel. QModelIndex model_index = get_model_index(Index(index.animation_name)); beginInsertRows(model_index, direction_nb, direction_nb); // Update our animation model list. int animation_nb = get_animation_nb(index); auto& directions = animations[animation_nb].directions; directions.insert( direction_nb, DirectionModel(index.animation_name, direction_nb)); // Update direction model indexes. for (int nb = direction_nb + 1; nb < directions.size(); nb++) { directions[nb].index->direction_nb = nb; } // Notify people before restoring the selection, so that they have a // chance to know new indexes before receiving selection signals. endInsertRows(); emit direction_added(Index(index.animation_name, direction_nb)); // Restore the selection. set_selected_index(selection); return direction_nb; }
/** * @brief Duplicates dialog(s). * @param id Id of the dialog to duplicate. * @param new_id Id of the duplicate dialog. * @throws EditorException in case of error. */ void DialogsModel::duplicate_dialogs( const QString& prefix, const QString& new_prefix) { // Check if dialogs can be duplicated. QString id; if (!can_duplicate_dialogs(prefix, new_prefix, id)) { throw EditorException(tr("Dialog '%1' already exists").arg(id)); } // Duplicate dialogs. for (QString id : get_ids(prefix)) { const auto& data = get_dialog_data(id); id.replace(QRegExp(QString("^") + prefix), new_prefix); create_dialog(id, data); } }
/** * @brief Creates a dialogs model. * @param quest The quest. * @param language_id Id of the language of dialogs to manage. * @param parent The parent object or nullptr. * @throws EditorException If the file could not be opened. */ DialogsModel::DialogsModel( const Quest& quest, const QString& language_id, QObject* parent) : QAbstractItemModel(parent), quest(quest), language_id(language_id), selection_model(this) { // Load the strings data file. QString path = quest.get_dialogs_path(language_id); if (!resources.import_from_file(path.toStdString())) { throw EditorException(tr("Cannot open dialogs data file '%1'").arg(path)); } // Create the indexed tree. build_dialog_tree(); }
/** * @brief Deletes a direction. * @param index Index of the direction to delete. * @throws EditorException in case of error. */ void SpriteModel::delete_direction(const Index &index) { // Make some checks first. if (!direction_exists(index)) { QString nb = std::to_string(index.direction_nb).c_str(); throw EditorException( tr("Direction %1 don't exists in animation '%2'").arg( nb, index.animation_name)); } // Save and clear the selection. Index selection = get_selected_index(); if (selection.animation_name == index.animation_name && selection.direction_nb == index.direction_nb) { selection.direction_nb = -1; } clear_selection(); // Delete the direction in the sprite file. get_animation(index).remove_direction(index.direction_nb); // Call beginRemoveRows() as requested by QAbstractItemModel. QModelIndex model_index = get_model_index(Index(index.animation_name)); beginRemoveRows(model_index, index.direction_nb, index.direction_nb); // Update our direction model list. int animation_nb = get_animation_nb(index); auto& directions = animations[animation_nb].directions; directions.removeAt(index.direction_nb); // Update direction model indexes. for (int nb = index.direction_nb; nb < directions.size(); nb++) { directions[nb].index->direction_nb = nb; } // Notify people before restoring the selection, so that they have a // chance to know new indexes before receiving selection signals. endRemoveRows(); emit direction_deleted(index); // Restore the selection. set_selected_index(selection); }
/** * @brief Adds a direction in an animation of this sprite. * @param index Index of the animation to add the direction. * @param frame The first frame of the direction to create. * @return Index of the created direction. * @throws EditorException in case of error. */ int SpriteModel::add_direction(const Index& index, const QRect& frame) { // Make some checks first. if (!animation_exists(index)) { throw EditorException( tr("Animation '%1' don't exists").arg(index.animation_name)); } // Save and clear the selection. Index selection = get_selected_index(); clear_selection(); // Add the direction to the sprite file. SpriteAnimationData& animation_data = get_animation(index); SpriteAnimationDirectionData direction( Point::to_solarus_point(frame.topLeft()), Size::to_solarus_size(frame.size())); animation_data.add_direction(direction); // Rebuild indexes in the list model (indexes were shifted). int direction_nb = animation_data.get_num_directions() - 1; // Call beginInsertRows() as requested by QAbstractItemModel. QModelIndex model_index = get_model_index(Index(index.animation_name)); beginInsertRows(model_index, direction_nb, direction_nb); // Update our animation model list. int animation_nb = get_animation_nb(index); animations[animation_nb].directions.append( DirectionModel(index.animation_name, direction_nb)); // Notify people before restoring the selection, so that they have a // chance to know new indexes before receiving selection signals. endInsertRows(); emit direction_added(Index(index.animation_name, direction_nb)); // Restore the selection. set_selected_index(selection); return direction_nb; }
/** * @brief Changes the prefix of dialog ids. * @param old_prefix The prefix key of strings to change. * @param new_prefix The new prefix to set. * @return The new ids of the dialogs. * @throws EditorException in case of error. */ QList<QPair<QString, QString>> DialogsModel::set_dialog_id_prefix( const QString& old_prefix, const QString& new_prefix) { // Check if the prefix can be changed. QString id; if (!can_set_dialog_id_prefix(old_prefix, new_prefix, id)) { throw EditorException(tr("Dialog '%1' already exists").arg(id)); } // change the dialog ids. QList<QPair<QString, QString>> list; for (QString old_id : get_ids(old_prefix)) { QString new_id = old_id; new_id.replace(QRegExp(QString("^") + old_prefix), new_prefix); list.push_back( QPair<QString, QString>(old_id, set_dialog_id(old_id, new_id))); } return list; }
/** * @brief Reload the current translation. */ void DialogsModel::reload_translation() { clear_translation_from_tree(); QString path = quest.get_dialogs_path(translation_id); translation_resources.clear(); if (!translation_resources.import_from_file(path.toStdString())) { translation_id = ""; throw EditorException(tr("Cannot open dialogs data file '%1'").arg(path)); } for (const auto& kvp : translation_resources.get_dialogs()) { QString id = QString::fromStdString(kvp.first); QString parent_id; int index; if (dialog_tree.add_ref(id, parent_id, index)) { beginInsertRows(id_to_index(parent_id), index, index); endInsertRows(); } } }
void RightCtrl::execute(EditorModel& model) { int max = model.line(model.cursorLine()).size(); if (model.cursorColumn() <= max) { model.moveCursorRight(); } else if (model.cursorColumn() == max + 1) { if (model.cursorLine() != model.lineCount()) { model.moveCursorDown(); model.setCursorHome(); } else { throw EditorException("Already at end"); } } }
/** * @brief Deletes a dialog. * * The index of multiple dialogs may change, since they are sorted alphabetically. * Emits rowsAboutToBeRemoved(), removes the dialog * and then emits rowsRemoved(), as required by QAbstractItemModel. * * Then, emits dialog_deleted(). * * Except for the deleted dialog, the existing selection is preserved, * though the index of many dialogs can change. * The selection is cleared before the operations and restored after, * updated with the new indexes. * * @param id Id of the dialog to delete. * @throws EditorException in case of error. */ void DialogsModel::delete_dialog(const QString& id) { // Make some checks first. if (!dialog_exists(id)) { throw EditorException(tr("Invalid dialog id: %1").arg(id)); } // Save and clear the selection since a lot of indexes may change. QString old_selection = get_selected_id(); clear_selection(); // Delete from the strings file resources.remove_dialog(id.toStdString()); // Remove from the indexed tree. QString parent_id; int index; if (dialog_tree.can_remove_key(id, parent_id, index)) { // Call beginRemoveRows() as requested by QAbstractItemModel. beginRemoveRows(id_to_index(parent_id), index, index); dialog_tree.remove_key(id); // Notify people before restoring the selection, so that they have a // chance to know new indexes before receiving selection signals. endRemoveRows(); } else if (dialog_tree.remove_key(id)) { QModelIndex model_index = id_to_index(id); dataChanged(model_index, model_index); } // Notify people. emit dialog_deleted(id); // Restore the selection. set_selected_id(old_selection); }
/** * @brief Creates a sprite model. * @param quest The quest. * @param sprite_id Id of the sprite to manage. * @param parent The parent object or nullptr. * @throws EditorException If the file could not be opened. */ SpriteModel::SpriteModel( Quest& quest, const QString& sprite_id, QObject* parent) : QAbstractItemModel(parent), quest(quest), sprite_id(sprite_id), selection_model(this) { // Load the sprite data file. QString path = quest.get_sprite_path(sprite_id); if (!sprite.import_from_file(path.toStdString())) { throw EditorException(tr("Cannot open sprite '%1'").arg(path)); } // Build the index map of animations. build_index_map(); // Create animations and directions models. for (const auto& kvp : names_to_indexes) { const QString& animation_name = kvp.first; AnimationModel animation(animation_name); int num_dir = get_animation(animation_name).get_num_directions(); for (int nb = 0; nb < num_dir; nb++) { animation.directions.append(DirectionModel(animation_name, nb)); } animations.append(animation); } // use the first tileset of the quest QStringList tilesets = quest.get_resources().get_elements(ResourceType::TILESET); if (tilesets.size() > 0) { tileset_id = tilesets[0]; } }
/** * @brief Creates a map editor. * @param quest The quest containing the file. * @param path Path of the map data file to open. * @param parent The parent object or nullptr. * @throws EditorException If the file could not be opened. */ MapEditor::MapEditor(Quest& quest, const QString& path, QWidget* parent) : Editor(quest, path, parent), map_id(), map(nullptr), entity_creation_toolbar(nullptr), status_bar(nullptr), ignore_tileset_selection_changes(false) { ui.setupUi(this); build_entity_creation_toolbar(); build_status_bar(); // Get the map. ResourceType resource_type; QString map_id; quest.check_exists(path); if (!quest.is_resource_element(path, resource_type, map_id) || resource_type != ResourceType::MAP) { throw EditorException(tr("File '%1' is not a map").arg(path)); } this->map_id = map_id; // Editor properties. set_title(tr("Map %1").arg(get_file_name_without_extension())); set_icon(QIcon(":/images/icon_resource_map.png")); set_close_confirm_message( tr("Map '%1' has been modified. Save changes?").arg(map_id)); set_select_all_supported(true); set_zoom_supported(true); get_view_settings().set_zoom(2.0); set_grid_supported(true); set_entity_type_visibility_supported(true); // Shortcuts. QAction* open_script_action = new QAction(this); open_script_action->setShortcut(tr("F4")); open_script_action->setShortcutContext(Qt::WindowShortcut); connect(open_script_action, SIGNAL(triggered(bool)), this, SLOT(open_script_requested())); addAction(open_script_action); // Open the file. map = new MapModel(quest, map_id, this); get_undo_stack().setClean(); // Prepare the gui. const int side_width = 350; ui.splitter->setSizes({ side_width, width() - side_width }); ui.map_side_splitter->setStretchFactor(0, 0); // Don't expand the map properties view ui.map_side_splitter->setStretchFactor(1, 1); // but only the tileset view. ui.tileset_field->set_resource_type(ResourceType::TILESET); ui.tileset_field->set_quest(quest); ui.music_field->set_resource_type(ResourceType::MUSIC); ui.music_field->set_quest(quest); ui.music_field->add_special_value("none", tr("<No music>"), 0); ui.music_field->add_special_value("same", tr("<Same as before>"), 1); ui.tileset_view->set_read_only(true); ui.map_view->set_map(map); ui.map_view->set_view_settings(get_view_settings()); ui.map_view->set_common_actions(&get_common_actions()); ui.size_field->config("x", 0, 99999, 8); ui.size_field->set_tooltips( tr("Width of the map in pixels"), tr("Height of the map in pixels")); ui.location_field->config(",", 0, 99999, 8); ui.location_field->set_tooltips( tr("Coordinates of the map in its world (useful to make adjacent scrolling maps)"), tr("Coordinates of the map in its world (useful to make adjacent scrolling maps)")); set_num_layers_visibility_supported(map->get_num_layers()); load_settings(); update(); // Make connections. connect(&get_resources(), SIGNAL(element_description_changed(ResourceType, QString, QString)), this, SLOT(update_description_to_gui())); connect(ui.description_field, SIGNAL(editingFinished()), this, SLOT(set_description_from_gui())); connect(ui.size_field, SIGNAL(editing_finished()), this, SLOT(change_size_requested())); connect(map, SIGNAL(size_changed(QSize)), this, SLOT(update_size_field())); connect(ui.world_check_box, SIGNAL(stateChanged(int)), this, SLOT(world_check_box_changed())); connect(ui.world_field, SIGNAL(editingFinished()), this, SLOT(change_world_requested())); connect(map, SIGNAL(world_changed(QString)), this, SLOT(update_world_field())); connect(ui.floor_check_box, SIGNAL(stateChanged(int)), this, SLOT(floor_check_box_changed())); connect(ui.floor_field, SIGNAL(editingFinished()), this, SLOT(change_floor_requested())); connect(map, SIGNAL(floor_changed(int)), this, SLOT(update_floor_field())); connect(ui.location_field, SIGNAL(editing_finished()), this, SLOT(change_location_requested())); connect(map, SIGNAL(location_changed(QPoint)), this, SLOT(update_location_field())); connect(ui.tileset_field, SIGNAL(activated(QString)), this, SLOT(tileset_selector_activated())); connect(map, SIGNAL(tileset_id_changed(QString)), this, SLOT(tileset_id_changed(QString))); connect(ui.tileset_refresh_button, SIGNAL(clicked()), map, SLOT(tileset_modified())); connect(ui.tileset_edit_button, SIGNAL(clicked()), this, SLOT(open_tileset_requested())); connect(ui.music_field, SIGNAL(activated(QString)), this, SLOT(music_selector_activated())); connect(map, SIGNAL(music_id_changed(QString)), this, SLOT(update_music_field())); connect(ui.open_script_button, SIGNAL(clicked()), this, SLOT(open_script_requested())); connect(ui.map_view, SIGNAL(edit_entity_requested(EntityIndex, EntityModelPtr&)), this, SLOT(edit_entity_requested(EntityIndex, EntityModelPtr&))); connect(ui.map_view, SIGNAL(move_entities_requested(EntityIndexes, QPoint, bool)), this, SLOT(move_entities_requested(EntityIndexes, QPoint, bool))); connect(ui.map_view, SIGNAL(resize_entities_requested(QMap<EntityIndex, QRect>, bool)), this, SLOT(resize_entities_requested(QMap<EntityIndex, QRect>, bool))); connect(ui.map_view, SIGNAL(convert_tiles_requested(EntityIndexes)), this, SLOT(convert_tiles_requested(EntityIndexes))); connect(ui.map_view, SIGNAL(set_entities_direction_requested(EntityIndexes, int)), this, SLOT(set_entities_direction_requested(EntityIndexes, int))); connect(ui.map_view, SIGNAL(set_entities_layer_requested(EntityIndexes, int)), this, SLOT(set_entities_layer_requested(EntityIndexes, int))); connect(ui.map_view, SIGNAL(bring_entities_to_front_requested(EntityIndexes)), this, SLOT(bring_entities_to_front_requested(EntityIndexes))); connect(ui.map_view, SIGNAL(bring_entities_to_back_requested(EntityIndexes)), this, SLOT(bring_entities_to_back_requested(EntityIndexes))); connect(ui.map_view, SIGNAL(add_entities_requested(AddableEntities&)), this, SLOT(add_entities_requested(AddableEntities&))); connect(ui.map_view, SIGNAL(remove_entities_requested(EntityIndexes)), this, SLOT(remove_entities_requested(EntityIndexes))); connect(ui.map_view->get_scene(), SIGNAL(selectionChanged()), this, SLOT(map_selection_changed())); }
/** * @brief Changes the name of an animation. * * The index of the selection may change, since animations are sorted * alphabetically. * In this case, emits rowsAboutToBeMoved(), changes the index * and then emits rowsMoved(), as required by QAbstractItemModel. * * Then, emits animation_name_changed(), no matter if the index has also changed. * * The selection is preserved, though the index of many animations can change. * The selection is cleared before the operations and restored after, * updated with the new index. * * @param index Index of an existing animation. * @param new_name The new name to set. * @throws EditorException in case of error. */ void SpriteModel::set_animation_name(const Index& index, const QString& new_name) { if (new_name == index.animation_name) { // Nothing to do. return; } // Make some checks first. if (!animation_exists(index)) { throw EditorException( tr("Animation '%1' don't exists").arg(index.animation_name)); } if (new_name.length() <= 0) { throw EditorException(tr("Animation name cannot be empty")); } if (animation_exists(new_name)) { throw EditorException(tr("Animation '%1' already exists").arg(new_name)); } // Save and clear the selection since a lot of indexes may change. Index selection = get_selected_index(); clear_selection(); int animation_nb = get_animation_nb(index); // Change the name in the sprite file. sprite.set_animation_name( index.animation_name.toStdString(), new_name.toStdString()); // Change the index in the list model (if the order has changed). build_index_map(); int new_animation_nb = get_animation_nb(new_name); // Call beginMoveRows() if the index changes, as requested by // QAbstractItemModel. if (new_animation_nb != animation_nb) { int above_row = new_animation_nb; if (new_animation_nb > animation_nb) { ++above_row; } beginMoveRows(QModelIndex(), animation_nb, animation_nb, QModelIndex(), above_row); // Update our animation model list. animations.move(animation_nb, new_animation_nb); } animations[new_animation_nb].set_animation_name(new_name); // Notify people before restoring the selection, so that they have a // chance to know new indexes before receiving selection signals. if (new_animation_nb != animation_nb) { endMoveRows(); } emit animation_name_changed(index, Index(new_name)); // Restore the selection. if (selection.animation_name == index.animation_name) { selection.animation_name = new_name; } set_selected_index(selection); }