/**
 * @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();
    }
  }
}
Example #9
0
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);
}