1 /*
2  * Copyright (C) 2014-2018 Christopho, Solarus - http://www.solarus-games.org
3  *
4  * Solarus Quest Editor is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * Solarus Quest Editor is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License along
15  * with this program. If not, see <http://www.gnu.org/licenses/>.
16  */
17 #include "color.h"
18 #include "editor_exception.h"
19 #include "quest.h"
20 #include "rectangle.h"
21 #include "pattern_scrolling_traits.h"
22 #include "tileset_model.h"
23 #include <QCryptographicHash>
24 #include <QDebug>
25 #include <QFile>
26 #include <QFileSystemWatcher>
27 #include <QIcon>
28 #include <QTimer>
29 
30 namespace SolarusEditor {
31 
32 using TilePatternData = Solarus::TilePatternData;
33 
34 /**
35  * @brief Creates a tileset model.
36  * @param quest The quest.
37  * @param tileset_id Id of the tileset to manage.
38  * @param parent The parent object or nullptr.
39  * @throws EditorException If the file could not be opened.
40  */
TilesetModel(Quest & quest,const QString & tileset_id,QObject * parent)41 TilesetModel::TilesetModel(
42     Quest& quest,
43     const QString& tileset_id,
44     QObject* parent) :
45   QAbstractListModel(parent),
46   quest(quest),
47   tileset_id(tileset_id),
48   data_file_watcher(),
49   image_file_watcher(),
50   auto_refresh_data_file(true),
51   selection_model(this) {
52 
53   Q_ASSERT(!tileset_id.isEmpty());
54 
55   data_file_watcher.addPath(quest.get_tileset_data_file_path(tileset_id));
56   connect(&data_file_watcher, &QFileSystemWatcher::fileChanged,
57           this, &TilesetModel::tileset_data_file_changing);
58 
59   image_file_watcher.addPath(quest.get_tileset_tiles_image_path(tileset_id));
60   connect(&image_file_watcher, &QFileSystemWatcher::fileChanged,
61           this, &TilesetModel::tileset_image_file_changing);
62 
63   load();
64   reload_patterns_image();
65 }
66 
67 /**
68  * @brief Returns the quest.
69  */
get_quest() const70 const Quest& TilesetModel::get_quest() const {
71   return quest;
72 }
73 
74 /**
75  * @overload Non-const version.
76  */
get_quest()77 Quest& TilesetModel::get_quest() {
78   return quest;
79 }
80 
81 /**
82  * @brief Returns the id of the tileset managed by this model.
83  * @return The tileset id.
84  */
get_tileset_id() const85 QString TilesetModel::get_tileset_id() const {
86   return tileset_id;
87 }
88 
89 /**
90  * @brief Returns whether to automatically refresh after changes on disk.
91  * @return @c true if the tileset automatically refreshes.
92  */
get_auto_refresh_data_file()93 bool TilesetModel::get_auto_refresh_data_file() {
94   return auto_refresh_data_file;
95 }
96 
97 /**
98  * @brief Sets whether to automatically refresh after changes on disk.
99  *
100  * Use the tileset_data_file_changed() signal if you want to do something
101  * specific instead of automatically refreshing the model.
102  *
103  * @param auto_refresh_data_file @c true to automatically refresh.
104  */
set_auto_refresh_data_file(bool auto_refresh_data_file)105 void TilesetModel::set_auto_refresh_data_file(bool auto_refresh_data_file) {
106   this->auto_refresh_data_file = auto_refresh_data_file;
107 }
108 
109 /**
110  * @brief Loads the tileset from its data file.
111  */
load()112 void TilesetModel::load() {
113 
114   QString path = quest.get_tileset_data_file_path(tileset_id);
115 
116   beginResetModel();
117   if (!tileset.import_from_file(path.toLocal8Bit().toStdString())) {
118     throw EditorException(tr("Cannot open tileset data file '%1'").arg(path));
119   }
120 
121   build_index_map();
122   patterns.clear();
123   for (const auto& kvp : ids_to_indexes) {
124     const QString& pattern_id = kvp.first;
125     patterns.append(PatternModel(pattern_id));
126   }
127 
128   endResetModel();
129 }
130 
131 /**
132  * @brief Saves the tileset to its data file.
133  * @throws EditorException If the file could not be saved.
134  */
save() const135 void TilesetModel::save() const {
136 
137   QString path = quest.get_tileset_data_file_path(tileset_id);
138 
139   std::string new_data;
140   tileset.export_to_buffer(new_data);
141 
142   // First read the existing file if it exists,
143   // to check if there are actual changes.
144   QFile file(path);
145   if (file.open(QIODevice::ReadOnly)) {
146       QCryptographicHash hasher(QCryptographicHash::Sha1);
147       hasher.addData(&file);
148       QByteArray old_file_hash = hasher.result();
149 
150       hasher.reset();
151       hasher.addData(new_data.data(), new_data.size());
152 
153       if (hasher.result() == old_file_hash) {
154         // The saved version is already up-to-date: nothing to save.
155         // Avoid unnecessary refreshes of the tileset.
156         return;
157       }
158       file.close();
159   }
160 
161   // Now write the file.
162   file.open(QIODevice::WriteOnly);
163   qint64 bytes = file.write(new_data.data(), new_data.size());
164   if (bytes != static_cast<qint64>(new_data.size())) {
165     throw EditorException(tr("Cannot save tileset data file '%1'").arg(path));
166   }
167 }
168 
169 /**
170  * @brief Called when the tileset data file is being modified on the filesystem.
171  */
tileset_data_file_changing()172 void TilesetModel::tileset_data_file_changing() {
173 
174   // To avoid any risk of duplicate refreshes.
175   QString path = quest.get_tileset_data_file_path(tileset_id);
176   data_file_watcher.removePath(path);
177 
178   QTimer::singleShot(100, this, [this, path]() {
179     if (auto_refresh_data_file) {
180       try {
181         load();
182       }
183       catch (const EditorException& ex) {
184         qWarning() << tr("Failed to refresh tileset: %1").arg(ex.get_message());
185       }
186     }
187 
188     // Watch the path again because saving the tileset might have removed it
189     // from the watcher.
190     data_file_watcher.addPath(path);
191 
192     emit tileset_data_file_changed();
193   });
194 }
195 
196 /**
197  * @brief Called when the tileset image file is being modified on the filesystem.
198  */
tileset_image_file_changing()199 void TilesetModel::tileset_image_file_changing() {
200 
201   // To avoid any risk of duplicate refreshes.
202   QString path = quest.get_tileset_tiles_image_path(tileset_id);
203   image_file_watcher.removePath(path);
204 
205   QTimer::singleShot(500, this, [this, path]() {
206 
207     // Watch the path again because saving the tileset might have removed it
208     // from the watcher.
209     image_file_watcher.addPath(path);
210 
211     reload_patterns_image();
212   });
213 }
214 
215 /**
216  * @brief Returns the tileset's background color.
217  * @return The background color.
218  */
get_background_color() const219 QColor TilesetModel::get_background_color() const {
220 
221   return Color::to_qcolor(tileset.get_background_color());
222 }
223 
224 /**
225  * @brief Sets the tileset's background color.
226  *
227  * Emits background_color_changed if there is a change.
228  *
229  * @param background_color The background color.
230  */
set_background_color(const QColor & background_color)231 void TilesetModel::set_background_color(const QColor& background_color) {
232 
233   const Solarus::Color solarus_color = Color::to_solarus_color(background_color);
234   if (solarus_color == tileset.get_background_color()) {
235     return;
236   }
237   tileset.set_background_color(solarus_color);
238   emit background_color_changed(background_color);
239 }
240 
241 /**
242  * @brief Returns the number of items in the list.
243  *
244  * This is the number of patterns of the tileset.
245  *
246  * @return The number of patterns.
247  */
rowCount(const QModelIndex &) const248 int TilesetModel::rowCount(const QModelIndex& /* parent */) const {
249   return tileset.get_num_patterns();
250 }
251 
252 /**
253  * @brief Returns the datat of an item for a given role.
254  * @param index Index of the item to get.
255  * @param role The wanted role.
256  * @return The data.
257  */
data(const QModelIndex & index,int role) const258 QVariant TilesetModel::data(const QModelIndex& index, int role) const {
259 
260   switch (role) {
261 
262   case Qt::DisplayRole:
263     return QVariant();  // No text: only the icon.
264     break;
265 
266   case Qt::DecorationRole:
267     // Show an icon representing the pattern.
268     return get_pattern_icon(index.row());
269     break;
270 
271   default:
272     break;
273   }
274 
275   return QVariant();
276 }
277 
278 /**
279  * @brief Returns the number of patterns in the tileset.
280  * @return The number of patterns.
281  */
get_num_patterns() const282 int TilesetModel::get_num_patterns() const {
283 
284   return tileset.get_num_patterns();
285 }
286 
287 /**
288  * @brief Returns whether there exists a pattern with the specified index.
289  * @param index A pattern index.
290  * @return @c true if such a pattern exists in the tileset.
291  */
pattern_exists(int index) const292 bool TilesetModel::pattern_exists(int index) const {
293 
294   return index >= 0 && index < patterns.size();
295 }
296 
297 /**
298  * @brief Returns whether there exists a pattern with the specified id.
299  * @param pattern_id A pattern id.
300  * @return @c true if such a pattern exists in the tileset.
301  */
pattern_exists(const QString & pattern_id) const302 bool TilesetModel::pattern_exists(const QString& pattern_id) const {
303 
304   return id_to_index(pattern_id) != -1;
305 }
306 
307 /**
308  * @brief Returns the given id, possibly modified to make it unique.
309  * @param pattern_id A candidate pattern id.
310  * @return The same value if it does not exist yet,
311  * or a modified value different from all patterns of this tileset.
312  */
get_unique_pattern_id(const QString & pattern_id) const313 QString TilesetModel::get_unique_pattern_id(const QString& pattern_id) const {
314 
315   if (!pattern_exists(pattern_id)) {
316     return pattern_id;
317   }
318 
319   // Extract the prefix.
320   int counter = 1;
321   QString prefix = pattern_id;
322   QStringList words = pattern_id.split('_');
323   if (words.size() > 1) {
324     QString last_word = words.last();
325     bool is_int = false;
326     counter = last_word.toInt(&is_int);
327     if (is_int) {
328       words.removeLast();
329       prefix = words.join('_');
330     }
331   }
332 
333   // Add the first available integer as suffix.
334   QString new_id;
335   do {
336     ++counter;
337     new_id = QString("%1_%2").arg(prefix).arg(counter);
338   } while (pattern_exists(new_id));
339 
340   return new_id;
341 }
342 
343 /**
344  * @brief Returns the list index of the specified pattern.
345  * @param pattern_id Id of a tile pattern
346  * @return The corresponding index in the list.
347  * Returns -1 there is no pattern with this id.
348  */
id_to_index(const QString & pattern_id) const349 int TilesetModel::id_to_index(const QString& pattern_id) const {
350 
351   auto it = ids_to_indexes.find(pattern_id);
352   if (it == ids_to_indexes.end()) {
353     return -1;
354   }
355   return it->second;
356 }
357 
358 /**
359  * @brief Returns the string id of the pattern at the specified list index.
360  * @param index An index in the list of patterns.
361  * @return The corresponding pattern id.
362  * Returns an empty string if there is no pattern at this index.
363  */
index_to_id(int index) const364 QString TilesetModel::index_to_id(int index) const {
365 
366   if (!pattern_exists(index)) {
367     return "";
368   }
369 
370   return patterns.at(index).id;
371 }
372 
373 /**
374  * @brief Builds or rebuilds the internal mapping that gives indexes from ids.
375  *
376  * Tile patterns are indexed by string ids, but the model also treats them
377  * as a linear list, so we need an additional integer index.
378  */
build_index_map()379 void TilesetModel::build_index_map() {
380 
381   // boost::multi_index_container could do that for us, but this feels a bit
382   // overkill to add a boost dependency just for this use case.
383 
384   ids_to_indexes.clear();
385 
386   // This is a bit tricky because we change the order of
387   // the map from the Solarus library to use natural order instead.
388 
389   const std::map<std::string, TilePatternData>& pattern_map =
390       tileset.get_patterns();
391   // First, put the string keys to have natural sort.
392   for (const auto& kvp : pattern_map) {
393     QString pattern_id = QString::fromStdString(kvp.first);
394     ids_to_indexes.insert(std::make_pair(pattern_id, 0));
395   }
396 
397   // Then, we can put the integer value.
398   int index = 0;
399   for (const auto& kvp : ids_to_indexes) {
400     QString pattern_id = kvp.first;
401     ids_to_indexes[pattern_id] = index;
402     ++index;
403   }
404 }
405 
406 /**
407  * @brief Creates a new pattern in this tileset with default properties.
408  *
409  * The index of multiple patterns in the pattern list may change, since
410  * patterns are sorted alphabetically.
411  * Emits rowsAboutToBeInserted(), adds the new pattern
412  * and then emits rowsInserted(), as required by QAbstractItemModel.
413  *
414  * Then, emits pattern_created().
415  *
416  * The newly created pattern is not initially selected.
417  * The existing selection is preserved, though the index of many patterns can
418  * change.
419  * The selection is cleared before the operations and restored after,
420  * updated with the new indexes.
421  *
422  * @param pattern_id Id of the pattern to create.
423  * @param frame Position of the pattern in the tileset image
424  * (it will be a single-frame pattern).
425  * @return Index of the created pattern.
426  * @throws EditorException in case of error.
427  */
create_pattern(const QString & pattern_id,const QRect & frame)428 int TilesetModel::create_pattern(const QString& pattern_id, const QRect& frame) {
429 
430   // Make some checks first.
431   if (!is_valid_pattern_id(pattern_id)) {
432       throw EditorException(tr("Invalid tile pattern id: '%1'").arg(pattern_id));
433   }
434 
435   if (id_to_index(pattern_id) != -1) {
436       throw EditorException(tr("Tile pattern '%1' already exists").arg(pattern_id));
437   }
438 
439   // Save and clear the selection since a lot of indexes may change.
440   const QModelIndexList& old_selected_indexes = selection_model.selection().indexes();
441   QStringList old_selection_ids;
442   for (const QModelIndex& old_selected_index : old_selected_indexes) {
443     old_selection_ids << index_to_id(old_selected_index.row());
444   }
445   clear_selection();
446 
447   // Add the pattern to the tileset file.
448   TilePatternData pattern(Rectangle::to_solarus_rect(frame));
449   tileset.add_pattern(pattern_id.toStdString(), pattern);
450 
451   // Rebuild indexes in the list model (indexes were shifted).
452   build_index_map();
453   int index = id_to_index(pattern_id);
454 
455   // Call beginInsertRows() as requested by QAbstractItemModel.
456   beginInsertRows(QModelIndex(), index, index);
457 
458   // Update our pattern model list.
459   patterns.insert(index, PatternModel(pattern_id));
460 
461   // Notify people before restoring the selection, so that they have a
462   // chance to know new indexes before receiving selection signals.
463   endInsertRows();
464   emit pattern_created(index, pattern_id);
465 
466   // Restore the selection.
467   for (QString selected_pattern_id : old_selection_ids) {
468     int new_index = id_to_index(selected_pattern_id);
469     add_to_selected(new_index);
470   }
471 
472   return index;
473 }
474 
475 /**
476  * @brief Deletes a tile pattern.
477  *
478  * The index of multiple patterns in the pattern list may change, since
479  * patterns are sorted alphabetically.
480  * Emits rowsAboutToBeRemoved(), removes the pattern
481  * and then emits rowsRemoved(), as required by QAbstractItemModel.
482  *
483  * Then, emits pattern_deleted().
484  *
485  * Except for the deleted pattern, the existing selection is preserved,
486  * though the index of many patterns can change.
487  * The selection is cleared before the operations and restored after,
488  * updated with the new indexes.
489  *
490  * If you have multiple patterns to delete, call delete_patterns()
491  * for better performance.
492  *
493  * @param index Index of the pattern to delete.
494  * @throws EditorException in case of error.
495  */
delete_pattern(int index)496 void TilesetModel::delete_pattern(int index) {
497 
498   QString pattern_id = index_to_id(index);
499 
500   // Make some checks first.
501   if (pattern_id.isEmpty()) {
502       throw EditorException(tr("Invalid tile pattern index: %1").arg(index));
503   }
504 
505   // Save and clear the selection since a lot of indexes may change.
506   const QModelIndexList& old_selected_indexes = selection_model.selection().indexes();
507   QStringList old_selection_ids;
508   for (const QModelIndex& old_selected_index : old_selected_indexes) {
509     old_selection_ids << index_to_id(old_selected_index.row());
510   }
511   clear_selection();
512 
513   // Delete the pattern in the tileset file.
514   tileset.remove_pattern(pattern_id.toStdString());
515 
516   // Rebuild indexes in the list model (indexes were shifted).
517   build_index_map();
518 
519   // Call beginRemoveRows() as requested by QAbstractItemModel.
520   beginRemoveRows(QModelIndex(), index, index);
521 
522   // Update our pattern model list.
523   patterns.removeAt(index);
524 
525   // Notify people before restoring the selection, so that they have a
526   // chance to know new indexes before receiving selection signals.
527   endRemoveRows();
528   emit pattern_deleted(index, pattern_id);
529 
530   // Restore the selection.
531   for (const QString& selected_pattern_id : old_selection_ids) {
532 
533     if (selected_pattern_id == pattern_id) {
534       // Exclude the deleted one.
535       continue;
536     }
537 
538     int new_index = id_to_index(selected_pattern_id);
539     add_to_selected(new_index);
540   }
541 }
542 
543 /**
544  * @brief Deletes some tile patterns.
545  *
546  * The index of multiple patterns in the pattern list may change, since
547  * patterns are sorted alphabetically.
548  * For each pattern, emits rowsAboutToBeRemoved(), removes the pattern,
549  * emits rowsRemoved() as required by QAbstractItemModel,
550  * and then, emits pattern_deleted().
551  *
552  * Except for the deleted patterns, the existing selection is preserved,
553  * though the index of many patterns can change.
554  * The selection is cleared before the operations and restored after,
555  * updated with the new indexes.
556  *
557  * @param indexes Indexes of the patterns to delete.
558  * @throws EditorException in case of error.
559  */
delete_patterns(const QList<int> & indexes)560 void TilesetModel::delete_patterns(const QList<int>& indexes) {
561 
562   // Make a sorted list of patterns with their index and id.
563   QList<QPair<int, QString>> sorted_patterns;
564   for (int index : indexes) {
565     QString pattern_id = index_to_id(index);
566     if (pattern_id.isEmpty()) {
567         throw EditorException(tr("Invalid tile pattern index: %1").arg(index));
568     }
569     sorted_patterns.append({ index, pattern_id });
570   }
571   std::sort(sorted_patterns.begin(), sorted_patterns.end(), [](const QPair<int, QString>& pattern_1, const QPair<int, QString>& pattern_2) {
572     return pattern_1.first < pattern_2.first;
573   });
574 
575   // Save and clear the selection during the whole operation.
576   const QModelIndexList old_selected_indexes = selection_model.selection().indexes();
577   QStringList old_selection_ids;
578   for (const QModelIndex& old_selected_index : old_selected_indexes) {
579     old_selection_ids << index_to_id(old_selected_index.row());
580   }
581   clear_selection();
582 
583   // Delete patterns, starting with the last one to avoid to shift any index.
584   beginResetModel();
585   for (auto it = sorted_patterns.rbegin(); it != sorted_patterns.rend(); ++it) {
586     int index = it->first;
587     QString pattern_id = it->second;
588     tileset.remove_pattern(pattern_id.toStdString());
589     patterns.removeAt(index);
590   }
591   build_index_map();
592   endResetModel();
593 
594   // Restore the selection.
595   for (QString selected_pattern_id : old_selection_ids) {
596     int new_index = id_to_index(selected_pattern_id);
597     if (new_index == -1) {
598       // This one was just deleted.
599       continue;
600     }
601     add_to_selected(new_index);
602   }
603 }
604 
605 /**
606  * @brief Changes the string id of a pattern.
607  *
608  * The index of multiple patterns in the pattern list may change, since
609  * patterns are sorted alphabetically.
610  * In this case, emits rowsAboutToBeMoved(), changes the index
611  * and then emits rowsMoved(), as required by QAbstractItemModel.
612  *
613  * Then, emits pattern_id_changed(), no matter if the index has also changed.
614  *
615  * The selection is preserved, though the index of many patterns can change.
616  * The selection is cleared before the operations and restored after,
617  * updated with the new indexes.
618  *
619  * @param index Index of an existing pattern.
620  * @param new_id The new id to set.
621  * @return The new index of the pattern.
622  * @throws EditorException in case of error.
623  */
set_pattern_id(int index,const QString & new_id)624 int TilesetModel::set_pattern_id(int index, const QString& new_id) {
625 
626   QString old_id = index_to_id(index);
627   if (new_id == old_id) {
628     // Nothing to do.
629     return index;
630   }
631 
632   // Make some checks first.
633   if (old_id.isEmpty()) {
634       throw EditorException(tr("Invalid tile pattern index: %1").arg(index));
635   }
636 
637   if (!is_valid_pattern_id(new_id)) {
638       throw EditorException(tr("Invalid tile pattern id: '%1'").arg(new_id));
639   }
640 
641   if (id_to_index(new_id) != -1) {
642       throw EditorException(tr("Tile pattern '%1' already exists").arg(new_id));
643   }
644 
645   // Save and clear the selection since a lot of indexes may change.
646   const QModelIndexList& old_selected_indexes = selection_model.selection().indexes();
647   QStringList old_selection_ids;
648   for (const QModelIndex& old_selected_index : old_selected_indexes) {
649     old_selection_ids << index_to_id(old_selected_index.row());
650   }
651   clear_selection();
652 
653   // Change the id in the tileset file.
654   tileset.set_pattern_id(old_id.toStdString(), new_id.toStdString());
655 
656   // Change the index in the list model (if the order has changed).
657   build_index_map();
658   int new_index = id_to_index(new_id);
659 
660   // Call beginMoveRows() if the index changes, as requested by
661   // QAbstractItemModel.
662   if (new_index != index) {
663     int above_row = new_index;
664     if (new_index > index) {
665       ++above_row;
666     }
667     beginMoveRows(QModelIndex(), index, index,
668                   QModelIndex(), above_row);
669 
670     // Update our pattern model list.
671     patterns.move(index, new_index);
672   }
673 
674   patterns[new_index].id = new_id;
675 
676   // Notify people before restoring the selection, so that they have a
677   // chance to know new indexes before receiving selection signals.
678   if (new_index != index) {
679     endMoveRows();
680   }
681   emit pattern_id_changed(index, old_id, new_index, new_id);
682 
683   // Restore the selection.
684   for (QString pattern_id : old_selection_ids) {
685     if (pattern_id == old_id) {
686       pattern_id = new_id;
687     }
688     int new_index = id_to_index(pattern_id);
689     add_to_selected(new_index);
690   }
691 
692   return new_index;
693 }
694 
695 /**
696  * @brief Returns whether a string is a valid tile pattern id.
697  * @param pattern_id The id to check.
698  * @return @c true if this id is legal.
699  */
is_valid_pattern_id(const QString & pattern_id)700 bool TilesetModel::is_valid_pattern_id(const QString& pattern_id) {
701 
702   if (pattern_id.isEmpty()) {
703       return false;
704   }
705 
706   if (
707       pattern_id.contains('\"') ||
708       pattern_id.contains('\'') ||
709       pattern_id.contains('\\') ||
710       pattern_id.contains('\n') ||
711       pattern_id.contains('\r')
712   ) {
713     return false;
714   }
715 
716   return true;
717 }
718 
719 /**
720  * @brief Returns whether a pattern is a multi-frame pattern.
721  * @param index A pattern index.
722  * @return @c true if this is a multi-frame pattern.
723  */
is_pattern_multi_frame(int index) const724 bool TilesetModel::is_pattern_multi_frame(int index) const {
725 
726   const std::string& pattern_id = index_to_id(index).toStdString();
727   return tileset.get_pattern(pattern_id)->is_multi_frame();
728 }
729 
730 /**
731  * @brief Returns whether the given patterns are all multi-frame.
732  * @param indexes A list of pattern indexes.
733  * @return @c true if all these patterns are multi-frame.
734  */
are_patterns_multi_frame(const QList<int> & indexes) const735 bool TilesetModel::are_patterns_multi_frame(const QList<int>& indexes) const {
736 
737   for (int index : indexes) {
738     if (!is_pattern_multi_frame(index)) {
739       return false;
740     }
741   }
742   return true;
743 }
744 
745 /**
746  * @brief Returns the number of frames of a pattern.
747  * @param index A pattern index.
748  * @return The number of frames in the tileset.
749  */
get_pattern_num_frames(int index) const750 int TilesetModel::get_pattern_num_frames(int index) const {
751 
752   const std::string& pattern_id = index_to_id(index).toStdString();
753   return tileset.get_pattern(pattern_id)->get_num_frames();
754 }
755 
756 /**
757  * @brief Gets the number of frames of the specified patterns if it is the same.
758  * @param[in] indexes A list of pattern indexes.
759  * @param[out] num_frames The common number of frames if any.
760  * The value is left unchanged if the frame count is not common.
761  * @return @c true if all specified patterns have the same number of frames.
762  * If the list is empty, @c false is returned.
763  */
is_common_pattern_num_frames(const QList<int> & indexes,int & num_frames) const764 bool TilesetModel::is_common_pattern_num_frames(const QList<int>& indexes, int& num_frames) const {
765 
766   if (indexes.empty()) {
767     return false;
768   }
769 
770   int candidate = get_pattern_num_frames(indexes.first());
771   for (int index : indexes) {
772     if (get_pattern_num_frames(index) != candidate) {
773       return false;
774     }
775   }
776 
777   num_frames = candidate;
778   return true;
779 }
780 
781 /**
782  * @brief Sets the number of frames of a pattern.
783  *
784  * Emits pattern_num_frames_changed() if there is a change.
785  *
786  * @param index A pattern index.
787  * @return The number of frames to set.
788  * @throws EditorException in case of error.
789  */
set_pattern_num_frames(int index,int num_frames)790 void TilesetModel::set_pattern_num_frames(int index, int num_frames) {
791 
792   if (num_frames <= 0) {
793     throw EditorException("Invalid number of frames");
794   }
795 
796   if (num_frames == get_pattern_num_frames(index)) {
797     return;
798   }
799 
800   const std::string& pattern_id = index_to_id(index).toStdString();
801   Solarus::TilePatternData& pattern = *tileset.get_pattern(pattern_id);
802 
803   Solarus::Rectangle first = pattern.get_frame();
804   std::vector<Solarus::Rectangle> frames(num_frames, first);
805   PatternSeparation separation = get_pattern_separation(index);
806   for (int i = 1; i < num_frames; ++i) {
807     if (separation == PatternSeparation::HORIZONTAL) {
808       frames[i].add_x(i * first.get_width());
809     }
810     else {
811       frames[i].add_y(i * first.get_height());
812     }
813   }
814   pattern.set_frames(frames);
815   patterns[index].set_image_dirty();
816 
817   emit pattern_num_frames_changed(index, num_frames);
818 }
819 
820 /**
821  * @brief Returns the coordinates of a pattern's frame in the tileset image.
822  * @param index A pattern index.
823  * @return The pattern's frame.
824  * If this is a multi-frame pattern, the first frame is returned.
825  */
get_pattern_frame(int index) const826 QRect TilesetModel::get_pattern_frame(int index) const {
827 
828   const std::string& pattern_id = index_to_id(index).toStdString();
829   const Solarus::Rectangle& frame = tileset.get_pattern(pattern_id)->get_frame();
830   return Rectangle::to_qrect(frame);
831 }
832 
833 /**
834  * @brief Returns the coordinates of all frames of a pattern in the tileset
835  * image.
836  * @param index A pattern index.
837  * @return The pattern's frames.
838  */
get_pattern_frames(int index) const839 QList<QRect> TilesetModel::get_pattern_frames(int index) const {
840 
841   const std::string& pattern_id = index_to_id(index).toStdString();
842   const std::vector<Solarus::Rectangle>& frames =
843       tileset.get_pattern(pattern_id)->get_frames();
844 
845   QList<QRect> result;
846   for (const Solarus::Rectangle& frame : frames) {
847     result << Rectangle::to_qrect(frame);
848   }
849   return result;
850 }
851 
852 /**
853  * @brief Returns the coordinates of the rectangle containing all frames of a
854  * pattern in the tileset image.
855  *
856  * For single-frame patterns, this gives the same result as get_pattern_frame().
857  *
858  * @param index A pattern index.
859  * @return The bounding box of the pattern's frames.
860  */
get_pattern_frames_bounding_box(int index) const861 QRect TilesetModel::get_pattern_frames_bounding_box(int index) const {
862 
863   QRect box = get_pattern_frame(index);
864   if (!is_pattern_multi_frame(index)) {
865     return box;
866   }
867 
868   int num_frames = get_pattern_num_frames(index);
869   if (get_pattern_separation(index) == PatternSeparation::HORIZONTAL) {
870     box.setWidth(box.width() * num_frames);
871   }
872   else {
873     box.setHeight(box.height() * num_frames);
874   }
875   return box;
876 }
877 
878 /**
879  * @brief Sets the coordinates of the rectangle containing all frames of a
880  * pattern in the tileset image.
881  *
882  * Emits pattern_position_changed() if there is a change.
883  *
884  * @param index A pattern index.
885  * @param position Top-left position of the first frame of the pattern.
886  */
set_pattern_position(int index,const QPoint & position)887 void TilesetModel::set_pattern_position(int index, const QPoint& position) {
888 
889   if (position == get_pattern_frame(index).topLeft()) {
890     // No change.
891     return;
892   }
893 
894   const std::string& pattern_id = index_to_id(index).toStdString();
895   std::vector<Solarus::Rectangle> frames = tileset.get_pattern(pattern_id)->get_frames();
896 
897   int old_x = frames[0].get_x();
898   int old_y = frames[0].get_y();
899   for (Solarus::Rectangle& frame : frames) {
900     int dx = frame.get_x() - old_x;
901     int dy = frame.get_y() - old_y;
902     frame.set_xy(position.x() + dx, position.y() + dy);
903   }
904 
905   tileset.get_pattern(pattern_id)->set_frames(frames);
906 
907   // The icon has changed.
908   patterns[index].set_image_dirty();
909 
910   // Notify people.
911   emit pattern_position_changed(index, position);
912 
913   QModelIndex model_index = this->index(index);
914   emit dataChanged(model_index, model_index);
915 }
916 
917 /**
918  * @brief Returns the ground of a pattern.
919  * @param index A pattern index.
920  * @return The pattern's ground.
921  */
get_pattern_ground(int index) const922 Ground TilesetModel::get_pattern_ground(int index) const {
923 
924   const std::string& pattern_id = index_to_id(index).toStdString();
925   return tileset.get_pattern(pattern_id)->get_ground();
926 }
927 
928 /**
929  * @brief Gets the ground of the specified patterns if it is the same.
930  * @param[in] indexes A list of pattern indexes.
931  * @param[out] ground The common ground if any.
932  * The value is left unchanged if the ground is not common.
933  * @return @c true if all specified patterns have the same ground.
934  * If the list is empty, @c false is returned.
935  */
is_common_pattern_ground(const QList<int> & indexes,Ground & ground) const936 bool TilesetModel::is_common_pattern_ground(const QList<int>& indexes, Ground& ground) const {
937 
938   if (indexes.empty()) {
939     return false;
940   }
941 
942   Ground candidate = get_pattern_ground(indexes.first());
943   for (int index : indexes) {
944     if (get_pattern_ground(index) != candidate) {
945       return false;
946     }
947   }
948 
949   ground = candidate;
950   return true;
951 }
952 
953 /**
954  * @brief Sets the ground of a tile pattern.
955  *
956  * Emits pattern_ground_changed() if there is a change.
957  *
958  * @param index A pattern index.
959  * @param ground The ground to set.
960  */
set_pattern_ground(int index,Ground ground)961 void TilesetModel::set_pattern_ground(int index, Ground ground) {
962 
963   Solarus::TilePatternData& pattern = *tileset.get_pattern(index_to_id(index).toStdString());
964   if (ground == pattern.get_ground()) {
965     return;
966   }
967   pattern.set_ground(ground);
968   emit pattern_ground_changed(index, ground);
969 }
970 
971 /**
972  * @brief Returns the default layer of a pattern.
973  * @param index A pattern index.
974  * @return The pattern's default layer.
975  */
get_pattern_default_layer(int index) const976 int TilesetModel::get_pattern_default_layer(int index) const {
977 
978   const std::string& pattern_id = index_to_id(index).toStdString();
979   return tileset.get_pattern(pattern_id)->get_default_layer();
980 }
981 
982 /**
983  * @brief Gets the default layer of the specified patterns if it is the same.
984  * @param[in] indexes A list of pattern indexes.
985  * @param[out] default_layer The common default layer if any.
986  * The value is left unchanged if the layer is not common.
987  * @return @c true if all specified patterns have the same default layer.
988  * If the list is empty, @c false is returned.
989  */
is_common_pattern_default_layer(const QList<int> & indexes,int & default_layer) const990 bool TilesetModel::is_common_pattern_default_layer(const QList<int>& indexes, int& default_layer) const {
991 
992   if (indexes.empty()) {
993     return false;
994   }
995 
996   int candidate = get_pattern_default_layer(indexes.first());
997   for (int index : indexes) {
998     if (get_pattern_default_layer(index) != candidate) {
999       return false;
1000     }
1001   }
1002 
1003   default_layer = candidate;
1004   return true;
1005 }
1006 
1007 /**
1008  * @brief Sets the default layer of a tile pattern.
1009  *
1010  * Emits pattern_default_layer_changed() if there is a change.
1011  *
1012  * @param index A pattern index.
1013  * @param default_layer The default layer to set.
1014  */
set_pattern_default_layer(int index,int default_layer)1015 void TilesetModel::set_pattern_default_layer(int index, int default_layer) {
1016 
1017   Solarus::TilePatternData& pattern = *tileset.get_pattern(index_to_id(index).toStdString());
1018   if (default_layer == pattern.get_default_layer()) {
1019     return;
1020   }
1021   pattern.set_default_layer(default_layer);
1022   emit pattern_default_layer_changed(index, default_layer);
1023 }
1024 
1025 /**
1026  * @brief Returns the repeat mode of a pattern.
1027  * @param index A pattern index.
1028  * @return The pattern's repeat mode.
1029  */
get_pattern_repeat_mode(int index) const1030 PatternRepeatMode TilesetModel::get_pattern_repeat_mode(int index) const {
1031 
1032   const std::string& pattern_id = index_to_id(index).toStdString();
1033   return tileset.get_pattern(pattern_id)->get_repeat_mode();
1034 }
1035 
1036 /**
1037  * @brief Gets the repeat mode of the specified patterns if it is the same.
1038  * @param[in] indexes A list of pattern indexes.
1039  * @param[out] repeat_mode The common repeat mode if any.
1040  * The value is left unchanged if the repeat mode is not common.
1041  * @return @c true if all specified patterns have the same repeat mode.
1042  * If the list is empty, @c false is returned.
1043  */
is_common_pattern_repeat_mode(const QList<int> & indexes,PatternRepeatMode & repeat_mode) const1044 bool TilesetModel::is_common_pattern_repeat_mode(
1045     const QList<int>& indexes,
1046     PatternRepeatMode& repeat_mode) const {
1047 
1048   if (indexes.empty()) {
1049     return false;
1050   }
1051 
1052   PatternRepeatMode candidate = get_pattern_repeat_mode(indexes.first());
1053   for (int index : indexes) {
1054     if (get_pattern_repeat_mode(index) != candidate) {
1055       return false;
1056     }
1057   }
1058 
1059   repeat_mode = candidate;
1060   return true;
1061 }
1062 
1063 /**
1064  * @brief Sets the repeat_mode of a tile pattern.
1065  *
1066  * Emits pattern_repeat_mode_changed() if there is a change.
1067  *
1068  * @param index A pattern index.
1069  * @param repeat_mode The repeat mode to set.
1070  */
set_pattern_repeat_mode(int index,PatternRepeatMode repeat_mode)1071 void TilesetModel::set_pattern_repeat_mode(int index, PatternRepeatMode repeat_mode) {
1072 
1073   Solarus::TilePatternData& pattern = *tileset.get_pattern(index_to_id(index).toStdString());
1074   if (repeat_mode == pattern.get_repeat_mode()) {
1075     return;
1076   }
1077   pattern.set_repeat_mode(repeat_mode);
1078   emit pattern_repeat_mode_changed(index, repeat_mode);
1079 }
1080 
1081 /**
1082  * @brief Returns the scrolling property of a pattern.
1083  * @param index A pattern index.
1084  * @return The pattern's scrolling.
1085  */
get_pattern_scrolling(int index) const1086 PatternScrolling TilesetModel::get_pattern_scrolling(int index) const {
1087 
1088   const std::string& pattern_id = index_to_id(index).toStdString();
1089   const Solarus::TilePatternData& pattern = *tileset.get_pattern(pattern_id);
1090 
1091   return pattern.get_scrolling();
1092 }
1093 
1094 /**
1095  * @brief Gets the scrolling property of the specified patterns if it is the same.
1096  * @param[in] indexes A list of pattern indexes.
1097  * @param[out] scrolling The common scrolling if any.
1098  * The value is left unchanged if the scrolling is not common.
1099  * @return @c true if all specified patterns have the same scrolling.
1100  * If the list is empty, @c false is returned.
1101  */
is_common_pattern_scrolling(const QList<int> & indexes,PatternScrolling & scrolling) const1102 bool TilesetModel::is_common_pattern_scrolling(const QList<int>& indexes, PatternScrolling& scrolling) const {
1103 
1104   if (indexes.empty()) {
1105     return false;
1106   }
1107 
1108   PatternScrolling candidate = get_pattern_scrolling(indexes.first());
1109   for (int index : indexes) {
1110     if (get_pattern_scrolling(index) != candidate) {
1111       return false;
1112     }
1113   }
1114 
1115   scrolling = candidate;
1116   return true;
1117 }
1118 
1119 /**
1120  * @brief Sets the scrolling property of a pattern.
1121  *
1122  * Emits pattern_scrolling_changed() if there is a change.
1123  *
1124  * @param index A pattern index.
1125  * @return The scrolling value to set.
1126  * @throws EditorException in case of error.
1127  */
set_pattern_scrolling(int index,PatternScrolling scrolling)1128 void TilesetModel::set_pattern_scrolling(int index, PatternScrolling scrolling) {
1129 
1130   PatternScrolling old_scrolling = get_pattern_scrolling(index);
1131   if (scrolling == old_scrolling) {
1132     return;
1133   }
1134 
1135   const std::string& pattern_id = index_to_id(index).toStdString();
1136   Solarus::TilePatternData& pattern = *tileset.get_pattern(pattern_id);
1137 
1138   pattern.set_scrolling(scrolling);
1139 
1140   emit pattern_scrolling_changed(index, scrolling);
1141 }
1142 
1143 /**
1144  * @brief Returns the separation of the frames if the pattern is multi-frame.
1145  * @param index A pattern index.
1146  * @return The type of separation of the frames.
1147  * Returns TilePatternSeparation::HORIZONTAL if the pattern is single-frame.
1148  */
get_pattern_separation(int index) const1149 PatternSeparation TilesetModel::get_pattern_separation(int index) const {
1150 
1151   const std::string& pattern_id = index_to_id(index).toStdString();
1152   const Solarus::TilePatternData& pattern = *tileset.get_pattern(pattern_id);
1153 
1154   const std::vector<Solarus::Rectangle>& frames = pattern.get_frames();
1155   if (frames.size() == 1) {
1156     return PatternSeparation::HORIZONTAL;
1157   }
1158 
1159   if (frames[0].get_y() == frames[1].get_y()) {
1160     return PatternSeparation::HORIZONTAL;
1161   }
1162   return PatternSeparation::VERTICAL;
1163 }
1164 
1165 /**
1166  * @brief Gets the separation of the specified patterns if it is the same.
1167  * @param[in] indexes A list of pattern indexes.
1168  * @param[out] separation The common separation if any.
1169  * The value is left unchanged if the separation is not common.
1170  * @return @c true if all specified patterns have the same separation.
1171  * If the list is empty, @c false is returned.
1172  */
is_common_pattern_separation(const QList<int> & indexes,PatternSeparation & separation) const1173 bool TilesetModel::is_common_pattern_separation(const QList<int>& indexes, PatternSeparation& separation) const {
1174 
1175   if (indexes.empty()) {
1176     return false;
1177   }
1178 
1179   PatternSeparation candidate = get_pattern_separation(indexes.first());
1180   for (int index : indexes) {
1181     if (get_pattern_separation(index) != candidate) {
1182       return false;
1183     }
1184   }
1185 
1186   separation = candidate;
1187   return true;
1188 }
1189 
1190 /**
1191  * Sets the type of separation of the frames if the tile pattern is multi-frame.
1192  *
1193  * Emits pattern_separation_changed() if there is a change.
1194  *
1195  * Nothing is done if the tile pattern is single-frame.
1196  *
1197  * @param index A pattern index.
1198  * @param separation The type of separation of the frames.
1199  * @throws EditorException If the separation is not valid, i.e. if the size of
1200  * each frame after separation is not divisible by 8.
1201  */
set_pattern_separation(int index,PatternSeparation separation)1202 void TilesetModel::set_pattern_separation(int index, PatternSeparation separation) {
1203 
1204   const std::string& pattern_id = index_to_id(index).toStdString();
1205   Solarus::TilePatternData& pattern = *tileset.get_pattern(pattern_id);
1206 
1207   if (!pattern.is_multi_frame()) {
1208     // Nothing to do.
1209     return;
1210   }
1211 
1212   PatternSeparation old_separation = get_pattern_separation(index);
1213   if (separation == old_separation) {
1214     // No change.
1215     return;
1216   }
1217 
1218   int num_frames = pattern.get_num_frames();
1219   Solarus::Rectangle first = pattern.get_frame();
1220   std::vector<Solarus::Rectangle> frames(num_frames, first);
1221   for (int i = 1; i < num_frames; ++i) {
1222     if (separation == PatternSeparation::HORIZONTAL) {
1223       frames[i].add_x(i * first.get_width());
1224     }
1225     else {
1226       frames[i].add_y(i * first.get_height());
1227     }
1228   }
1229   pattern.set_frames(frames);
1230 
1231   patterns[index].set_image_dirty();
1232 
1233   emit pattern_separation_changed(index, separation);
1234 }
1235 
1236 /**
1237  * @brief Returns the delay between frames for a multi-frame pattern.
1238  * @param index A pattern index.
1239  * @return The frame delay in milliseconds.
1240  */
get_pattern_frame_delay(int index) const1241 int TilesetModel::get_pattern_frame_delay(int index) const {
1242 
1243   const std::string& pattern_id = index_to_id(index).toStdString();
1244   const Solarus::TilePatternData& pattern = *tileset.get_pattern(pattern_id);
1245   return pattern.get_frame_delay();
1246 }
1247 
1248 /**
1249  * @brief Gets the frame delay of the specified patterns if it is the same.
1250  * @param[in] indexes A list of pattern indexes.
1251  * @param[out] frame_delay The common frame delay if any.
1252  * The value is left unchanged if the frame delay is not common
1253  * or if all patterns are not multi-frame.
1254  * @return @c true if all specified patterns are multi-frame and have the
1255  * same frame delay.
1256  * If the list is empty, @c false is returned.
1257  */
is_common_pattern_frame_delay(const QList<int> & indexes,int & frame_delay) const1258 bool TilesetModel::is_common_pattern_frame_delay(const QList<int>& indexes, int& frame_delay) const {
1259 
1260   if (indexes.empty()) {
1261     return false;
1262   }
1263 
1264   int candidate = get_pattern_frame_delay(indexes.first());
1265   for (int index : indexes) {
1266     if (!is_pattern_multi_frame(index) ||
1267         get_pattern_frame_delay(index) != candidate) {
1268       return false;
1269     }
1270   }
1271 
1272   frame_delay = candidate;
1273   return true;
1274 }
1275 
1276 /**
1277  * Sets the frame delay of a multi-tile pattern.
1278  *
1279  * Emits pattern_frame_delay_changed() if there is a change.
1280  *
1281  * Nothing is done if the tile pattern is single frame.
1282  *
1283  * @param index A pattern index.
1284  * @param frame_delay The frame delay to set in milliseconds.
1285  * @throws EditorException In case of error.
1286  */
set_pattern_frame_delay(int index,int frame_delay)1287 void TilesetModel::set_pattern_frame_delay(int index, int frame_delay) {
1288 
1289   if (frame_delay <= 0) {
1290     throw EditorException("Invalid frame delay");
1291   }
1292 
1293   if (frame_delay == get_pattern_frame_delay(index)) {
1294     return;
1295   }
1296 
1297   if (!is_pattern_multi_frame(index)) {
1298     return;
1299   }
1300 
1301   const std::string& pattern_id = index_to_id(index).toStdString();
1302   Solarus::TilePatternData& pattern = *tileset.get_pattern(pattern_id);
1303 
1304   pattern.set_frame_delay(frame_delay);
1305 
1306   emit pattern_frame_delay_changed(index, frame_delay);
1307 }
1308 
1309 /**
1310  * @brief Returns whether a multi-frame patterns does a mirror loop.
1311  * @param index A pattern index.
1312  * @return @c true if the animation plays backwards when looping.
1313  */
is_pattern_mirror_loop(int index) const1314 bool TilesetModel::is_pattern_mirror_loop(int index) const {
1315 
1316   const std::string& pattern_id = index_to_id(index).toStdString();
1317   const Solarus::TilePatternData& pattern = *tileset.get_pattern(pattern_id);
1318   return pattern.is_mirror_loop();
1319 }
1320 
1321 /**
1322  * @brief Gets the mirror loop property of patterns if it is the same.
1323  * @param[in] indexes A list of pattern indexes.
1324  * @param[out] mirror_loop The common mirror loop value if any.
1325  * The value is left unchanged if mirror loop is not common
1326  * or if all patterns are not multi-frame.
1327  * @return @c true if all specified patterns are multi-frame and have the
1328  * same mirror loop value.
1329  * If the list is empty, @c false is returned.
1330  */
is_common_pattern_mirror_loop(const QList<int> & indexes,bool & mirror_loop) const1331 bool TilesetModel::is_common_pattern_mirror_loop(const QList<int>& indexes, bool& mirror_loop) const {
1332 
1333   if (indexes.empty()) {
1334     return false;
1335   }
1336 
1337   bool candidate = is_pattern_mirror_loop(indexes.first());
1338   for (int index : indexes) {
1339     if (!is_pattern_multi_frame(index) ||
1340         is_pattern_mirror_loop(index) != candidate) {
1341       return false;
1342     }
1343   }
1344 
1345   mirror_loop = candidate;
1346   return true;
1347 }
1348 
1349 /**
1350  * Sets the mirror loop property of a multi-tile pattern.
1351  *
1352  * Emits pattern_mirror_loop_changed() if there is a change.
1353  *
1354  * Nothing is done if the tile pattern is single-frame.
1355  *
1356  * @param index A pattern index.
1357  * @param mirror_loop Whether to play the animation backwards when looping.
1358  */
set_pattern_mirror_loop(int index,bool mirror_loop)1359 void TilesetModel::set_pattern_mirror_loop(int index, bool mirror_loop) {
1360 
1361   if (mirror_loop == is_pattern_mirror_loop(index)) {
1362     return;
1363   }
1364 
1365   if (!is_pattern_multi_frame(index)) {
1366     return;
1367   }
1368 
1369   const std::string& pattern_id = index_to_id(index).toStdString();
1370   Solarus::TilePatternData& pattern = *tileset.get_pattern(pattern_id);
1371 
1372   pattern.set_mirror_loop(mirror_loop);
1373 
1374   emit pattern_mirror_loop_changed(index, mirror_loop);
1375 }
1376 
1377 /**
1378  * @brief Returns an image representing the specified pattern.
1379  *
1380  * The image has the size of the pattern.
1381  * If the pattern is multi-frame, the image of the first frame is returned.
1382  *
1383  * @param index Index of a tile pattern.
1384  * @return The corresponding image.
1385  * Returns a null pixmap if the tileset image is not loaded.
1386  */
get_pattern_image(int index) const1387 QPixmap TilesetModel::get_pattern_image(int index) const {
1388 
1389   if (!pattern_exists(index)) {
1390     // No such pattern.
1391     return QPixmap();
1392   }
1393 
1394   if (patterns_image.isNull()) {
1395     // No tileset image.
1396     return QPixmap();
1397   }
1398 
1399   const PatternModel& pattern = patterns.at(index);
1400   if (!pattern.image.isNull()) {
1401     // Image already created.
1402     return pattern.image;
1403   }
1404 
1405   // Lazily create the image.
1406   QRect frame = get_pattern_frame(index);
1407   QImage image = patterns_image.copy(frame);
1408 
1409   pattern.image = QPixmap::fromImage(image);
1410   return pattern.image;
1411 }
1412 
1413 /**
1414  * @brief Returns an image representing the specified pattern.
1415  *
1416  * If the pattern is multi-frame, the image returned contains all frames.
1417  *
1418  * @param index Index of a tile pattern.
1419  * @return The corresponding image with all frames.
1420  * Returns a null pixmap if the tileset image is not loaded.
1421  */
get_pattern_image_all_frames(int index) const1422 QPixmap TilesetModel::get_pattern_image_all_frames(int index) const {
1423 
1424   if (!pattern_exists(index)) {
1425     // No such pattern.
1426     return QPixmap();
1427   }
1428 
1429   if (!is_pattern_multi_frame(index)) {
1430     // Single frame pattern.
1431     return get_pattern_image(index);
1432   }
1433 
1434   if (patterns_image.isNull()) {
1435     // No tileset image.
1436     return QPixmap();
1437   }
1438 
1439   const PatternModel& pattern = patterns.at(index);
1440   if (!pattern.image_all_frames.isNull()) {
1441     // Image already created.
1442     return pattern.image_all_frames;
1443   }
1444 
1445   // Lazily create the image.
1446   QRect frame = get_pattern_frames_bounding_box(index);
1447   QImage image_all_frames = patterns_image.copy(frame);
1448 
1449   pattern.image_all_frames = QPixmap::fromImage(image_all_frames);
1450   return pattern.image_all_frames;
1451 }
1452 
1453 /**
1454  * @brief Returns a 32x32 icon representing the specified pattern.
1455  * @param index Index of a tile pattern.
1456  * @return The corresponding icon.
1457  * Returns a null pixmap if the tileset image is not loaded.
1458  */
get_pattern_icon(int index) const1459 QPixmap TilesetModel::get_pattern_icon(int index) const {
1460 
1461   QPixmap pixmap = get_pattern_image(index);
1462 
1463   if (pixmap.isNull()) {
1464     // No image available.
1465     return QPixmap();
1466   }
1467 
1468   const PatternModel& pattern = patterns.at(index);
1469   if (!pattern.icon.isNull()) {
1470     // Icon already created.
1471     return pattern.icon;
1472   }
1473 
1474   // Lazily create the icon.
1475   QImage image = pixmap.toImage();
1476   // Make sure we have an alpha channel.
1477   image = image.convertToFormat(QImage::Format_RGBA8888_Premultiplied);
1478 
1479   if (image.height() <= 16 && image.width() <= 16) {
1480     image = image.scaledToHeight(image.height() * 2);
1481   }
1482   else if (image.height() > 32) {
1483     image = image.scaledToHeight(32);
1484   }
1485 
1486   // Center the pattern in a 32x32 pixmap.
1487   int dx = (32 - image.width()) / 2;
1488   int dy = (32 - image.height()) / 2;
1489   image = image.copy(-dx, -dy, 32, 32);
1490 
1491   pattern.icon = QPixmap::fromImage(image);
1492   return pattern.icon;
1493 }
1494 
1495 /**
1496  * @brief Returns the PNG image of all tile patterns.
1497  * @return The patterns image.
1498  * Returns a null image if the tileset image is not loaded.
1499  */
get_patterns_image() const1500 QImage TilesetModel::get_patterns_image() const {
1501   return patterns_image;
1502 }
1503 
1504 /**
1505  * @brief Loads the tileset image from its PNG file.
1506  */
reload_patterns_image()1507 void TilesetModel::reload_patterns_image() {
1508 
1509   QString path = quest.get_tileset_tiles_image_path(tileset_id);
1510   patterns_image = QImage(path);
1511 
1512   for (PatternModel& pattern : patterns) {
1513     pattern.set_image_dirty();
1514   }
1515 
1516   emit tileset_image_file_reloaded();
1517 }
1518 
1519 /**
1520  * @brief Returns the selection model of the tileset.
1521  * @return The selection info.
1522  */
get_selection_model()1523 QItemSelectionModel& TilesetModel::get_selection_model() {
1524   return selection_model;
1525 }
1526 
1527 /**
1528  * @brief Returns whether no patterns are selected.
1529  * @return @c true if the selection is empty.
1530  */
is_selection_empty() const1531 bool TilesetModel::is_selection_empty() const {
1532 
1533   return selection_model.selection().isEmpty();
1534 }
1535 
1536 /**
1537  * @brief Returns the number of selected patterns.
1538  * @return The number of selected pattern.
1539  */
get_selection_count() const1540 int TilesetModel::get_selection_count() const {
1541 
1542   return selection_model.selection().count();
1543 }
1544 
1545 /**
1546  * @brief Returns the index of the selected pattern.
1547  * @return The selected pattern index.
1548  * Returns -1 if no pattern is selected or if multiple patterns are selected.
1549  */
get_selected_index() const1550 int TilesetModel::get_selected_index() const {
1551 
1552   QModelIndexList selected_indexes = selection_model.selectedIndexes();
1553   if (selected_indexes.size() != 1) {
1554     return -1;
1555   }
1556   return selected_indexes.first().row();
1557 }
1558 
1559 /**
1560  * @brief Returns the id of the selected pattern.
1561  * @return The selected pattern id.
1562  * Returns an empty string if no pattern is selected or if multiple patterns
1563  * are selected.
1564  */
get_selected_id() const1565 QString TilesetModel::get_selected_id() const {
1566   return index_to_id(get_selected_index());
1567 }
1568 
1569 /**
1570  * @brief Returns all selected pattern indexes.
1571  * @return The selected pattern indexes.
1572  */
get_selected_indexes() const1573 QList<int> TilesetModel::get_selected_indexes() const {
1574 
1575   QList<int> result;
1576   const QModelIndexList& selected_indexes = selection_model.selectedIndexes();
1577   for (const QModelIndex& index : selected_indexes) {
1578     result << index.row();
1579   }
1580   return result;
1581 }
1582 
1583 /**
1584  * @brief Returns all selected pattern ids.
1585  * @return The selected pattern ids.
1586  */
get_selected_ids() const1587 QStringList TilesetModel::get_selected_ids() const {
1588 
1589   QStringList result;
1590   const QModelIndexList& selected_indexes = selection_model.selectedIndexes();
1591   for (const QModelIndex& index : selected_indexes) {
1592     result << index_to_id(index.row());
1593   }
1594   return result;
1595 }
1596 
1597 /**
1598  * @brief Selects a pattern and deselects all others.
1599  * @param index The index to select.
1600  */
set_selected_index(int index)1601 void TilesetModel::set_selected_index(int index) {
1602 
1603   set_selected_indexes({ index });
1604 }
1605 
1606 /**
1607  * @brief Selects the specified patterns and deselects others.
1608  * @param indexes The indexes to select.
1609  */
set_selected_indexes(const QList<int> & indexes)1610 void TilesetModel::set_selected_indexes(const QList<int>& indexes) {
1611 
1612   const QModelIndexList& current_selection = selection_model.selectedIndexes();
1613 
1614   QItemSelection selection;
1615   for (int index : indexes) {
1616     QModelIndex model_index = this->index(index);
1617     selection.select(model_index, model_index);
1618   }
1619 
1620   if (selection.indexes().toSet() == current_selection.toSet()) {
1621     // No change.
1622     return;
1623   }
1624 
1625   selection_model.select(selection, QItemSelectionModel::ClearAndSelect);
1626 }
1627 
1628 /**
1629  * @brief Selects a pattern and lets the rest of the selection unchanged.
1630  * @param index The index to select.
1631  */
add_to_selected(int index)1632 void TilesetModel::add_to_selected(int index) {
1633 
1634   add_to_selected(QList<int>({ index }));
1635 }
1636 
1637 /**
1638  * @brief Selects the specified patterns and lets the rest of the selection
1639  * unchanged.
1640  * @param indexes The indexes to select.
1641  */
add_to_selected(const QList<int> & indexes)1642 void TilesetModel::add_to_selected(const QList<int>& indexes) {
1643 
1644   QItemSelection selection;
1645   for (int index : indexes) {
1646     QModelIndex model_index = this->index(index);
1647     selection.select(model_index, model_index);
1648   }
1649 
1650   selection_model.select(selection, QItemSelectionModel::Select);
1651 }
1652 
1653 /**
1654  * @brief Returns whether a pattern is selected.
1655  * @param index A pattern index.
1656  * @return @c true if this pattern is selected.
1657  */
is_selected(int index) const1658 bool TilesetModel::is_selected(int index) const {
1659 
1660   return selection_model.isSelected(this->index(index));
1661 }
1662 
1663 /**
1664  * @brief Changes the selection state of an item.
1665  * @param index Index of the pattern to toggle.
1666  */
toggle_selected(int index)1667 void TilesetModel::toggle_selected(int index) {
1668 
1669   selection_model.select(this->index(index), QItemSelectionModel::Toggle);
1670 }
1671 
1672 /**
1673  * @brief Selects all patterns of the tileset.
1674  */
select_all()1675 void TilesetModel::select_all() {
1676 
1677   QItemSelection selection;
1678   QModelIndex first_index = this->index(0);
1679   QModelIndex last_index = this->index(get_num_patterns() - 1);
1680   selection.select(first_index, last_index);
1681   selection_model.select(selection, QItemSelectionModel::Select);
1682 }
1683 
1684 /**
1685  * @brief Deselects all selected items.
1686  */
clear_selection()1687 void TilesetModel::clear_selection() {
1688 
1689   selection_model.clear();
1690 }
1691 
1692 /**
1693  * @brief Returns the number of border sets in this tileset.
1694  * @return The number of border sets.
1695  */
get_num_border_sets() const1696 int TilesetModel::get_num_border_sets() const {
1697 
1698   return static_cast<int>(tileset.get_border_sets().size());
1699 }
1700 
1701 /**
1702  * @brief Returns the ids of all border sets in this tileset.
1703  * @return The border set ids in alphabetical order.
1704  */
get_border_set_ids() const1705 QStringList TilesetModel::get_border_set_ids() const {
1706 
1707   const std::map<std::string, Solarus::BorderSet>& border_sets = tileset.get_border_sets();
1708   QStringList border_set_ids;
1709   for (const auto& kvp : border_sets) {
1710     border_set_ids << QString::fromStdString(kvp.first);
1711   }
1712   return border_set_ids;
1713 }
1714 
1715 /**
1716  * @brief Returns whether a border set exists with the given id.
1717  * @param border_set_id A border set id.
1718  * @return @c true if the tileset contains such a border set.
1719  */
border_set_exists(const QString & border_set_id) const1720 bool TilesetModel::border_set_exists(const QString& border_set_id) const {
1721 
1722   return tileset.border_set_exists(border_set_id.toStdString());
1723 }
1724 
1725 /**
1726  * @brief Creates an empty border set with the given id.
1727  *
1728  * Emits border_set_created().
1729  *
1730  * @param border_set_id A border set id.
1731  * @throws EditorException in case of error.
1732  */
create_border_set(const QString & border_set_id)1733 void TilesetModel::create_border_set(const QString& border_set_id) {
1734 
1735   if (border_set_exists(border_set_id)) {
1736     throw EditorException(tr("Contour already exists: '%1'").arg(border_set_id));
1737   }
1738 
1739   if (!is_valid_border_set_id(border_set_id)) {
1740       throw EditorException(tr("Invalid contour id: '%1'").arg(border_set_id));
1741   }
1742 
1743   bool success = tileset.add_border_set(border_set_id.toStdString(), Solarus::BorderSet());
1744 
1745   if (!success) {
1746     throw EditorException(tr("Failed to create contour '%1'").arg(border_set_id));
1747   }
1748 
1749   emit border_set_created(border_set_id);
1750 }
1751 
1752 /**
1753  * @brief Deletes a border set.
1754  *
1755  * Emits border_set_deleted().
1756  *
1757  * @param border_set_id Id of the border set to delete.
1758  * @throws EditorException in case of error.
1759  */
delete_border_set(const QString & border_set_id)1760 void TilesetModel::delete_border_set(const QString& border_set_id) {
1761 
1762   if (!border_set_exists(border_set_id)) {
1763     throw EditorException(tr("No such contour: '%1'").arg(border_set_id));
1764   }
1765 
1766   bool success = tileset.remove_border_set(border_set_id.toStdString());
1767 
1768   if (!success) {
1769     throw EditorException(tr("Failed to delete contour '%1'").arg(border_set_id));
1770   }
1771 
1772   emit border_set_deleted(border_set_id);
1773 }
1774 
1775 /**
1776  * @brief Renames a border set.
1777  *
1778  * Emits border_set_id_changed().
1779  *
1780  * @param old_id Id of an existing border set.
1781  * @param new_id New id to set.
1782  * @throws EditorException in case of error.
1783  */
set_border_set_id(const QString & old_id,const QString & new_id)1784 void TilesetModel::set_border_set_id(const QString& old_id, const QString& new_id) {
1785 
1786   if (!border_set_exists(old_id)) {
1787     throw EditorException(tr("No such contour: '%1'").arg(old_id));
1788   }
1789 
1790   if (!is_valid_border_set_id(new_id)) {
1791       throw EditorException(tr("Invalid contour id: '%1'").arg(new_id));
1792   }
1793 
1794   if (border_set_exists(new_id)) {
1795     throw EditorException(tr("Contour id already in use: '%1'").arg(new_id));
1796   }
1797 
1798   bool success = tileset.set_border_set_id(old_id.toStdString(), new_id.toStdString());
1799 
1800   if (!success) {
1801     throw EditorException(tr("Failed to rename contour '%1'").arg(old_id));
1802   }
1803 
1804   emit border_set_id_changed(old_id, new_id);
1805 }
1806 
1807 /**
1808  * @brief Returns the pattern id of a border for the given border set.
1809  * @param border_set_id A border set id.
1810  * @param border_kind The kind of border to get in this border set.
1811  * @return The pattern id of this border,
1812  * or an empty string if no pattern is set for this border kind.
1813  * @throws EditorException in case of error.
1814  */
get_border_set_pattern(const QString & border_set_id,BorderKind border_kind) const1815 QString TilesetModel::get_border_set_pattern(const QString& border_set_id, BorderKind border_kind) const {
1816 
1817   if (!border_set_exists(border_set_id)) {
1818     throw EditorException(tr("No such contour: '%1'").arg(border_set_id));
1819   }
1820 
1821   const Solarus::BorderSet& border_set = tileset.get_border_set(border_set_id.toStdString());
1822   return QString::fromStdString(border_set.get_pattern(border_kind));
1823 }
1824 
1825 /**
1826  * @brief Sets a pattern to a border for the given border set.
1827  *
1828  * Emits border_set_pattern_changed() if there is a change.
1829  *
1830  * @param border_set_id A border set id.
1831  * @param border_kind The kind of border to set in this border set.
1832  * @param pattern_id The pattern id to set for this border,
1833  * or an empty string to unset it.
1834  * @throws EditorException in case of error.
1835  */
set_border_set_pattern(const QString & border_set_id,BorderKind border_kind,const QString & pattern_id)1836 void TilesetModel::set_border_set_pattern(const QString& border_set_id, BorderKind border_kind, const QString& pattern_id) {
1837 
1838   if (!border_set_exists(border_set_id)) {
1839     throw EditorException(tr("No such contour: '%1'").arg(border_set_id));
1840   }
1841 
1842   if (pattern_id == get_border_set_pattern(border_set_id, border_kind)) {
1843     // No change.
1844     return;
1845   }
1846 
1847   Solarus::BorderSet& border_set = tileset.get_border_set(border_set_id.toStdString());
1848   border_set.set_pattern(border_kind, pattern_id.toStdString());
1849 
1850   emit border_set_pattern_changed(border_set_id, border_kind, pattern_id);
1851 }
1852 
1853 /**
1854  * @brief Returns whether a pattern is defined for a border in the given border set.
1855  * @param border_set_id A border set id.
1856  * @param border_kind The kind of border to get in this border set.
1857  * @return @c true if a pattern is defined for this border.
1858  * @throws EditorException in case of error.
1859  */
has_border_set_pattern(const QString & border_set_id,BorderKind border_kind) const1860 bool TilesetModel::has_border_set_pattern(const QString& border_set_id, BorderKind border_kind) const {
1861 
1862   if (!border_set_exists(border_set_id)) {
1863     throw EditorException(tr("No such contour: '%1'").arg(border_set_id));
1864   }
1865 
1866   return tileset.get_border_set(border_set_id.toStdString()).has_pattern(border_kind);
1867 }
1868 
1869 /**
1870  * @brief Returns the list of border patterns for the given border set.
1871  * @param border_set_id A border set id.
1872  * @return The pattern ids in the order of the BorderKind enum.
1873  * @throws EditorException in case of error.
1874  */
get_border_set_patterns(const QString & border_set_id) const1875 QStringList TilesetModel::get_border_set_patterns(const QString& border_set_id) const {
1876 
1877   if (!border_set_exists(border_set_id)) {
1878     throw EditorException(tr("No such contour: '%1'").arg(border_set_id));
1879   }
1880 
1881   const Solarus::BorderSet& border_set = tileset.get_border_set(border_set_id.toStdString());
1882   QStringList patterns;
1883   for (int i = 0; i < 12; ++i) {
1884     BorderKind border_kind = static_cast<BorderKind>(i);
1885     patterns << QString::fromStdString(border_set.get_pattern(border_kind));
1886   }
1887 
1888   return patterns;
1889 }
1890 
1891 /**
1892  * @brief Sets the list of border patterns for the given border set.
1893  *
1894  * Emits border_set_pattern_changed() for each pattern that changes.
1895  *
1896  * @param border_set_id A border set id.
1897  * @param patterns The pattern ids in the order of the BorderKind enum.
1898  * @throws EditorException in case of error.
1899  */
set_border_set_patterns(const QString & border_set_id,const QStringList & patterns)1900 void TilesetModel::set_border_set_patterns(
1901     const QString& border_set_id,
1902     const QStringList& patterns
1903 ) {
1904 
1905   if (!border_set_exists(border_set_id)) {
1906     throw EditorException(tr("No such contour: '%1'").arg(border_set_id));
1907   }
1908 
1909   for (int i = 0; i < 12; ++i) {
1910     BorderKind border_kind = static_cast<BorderKind>(i);
1911     if (i >= patterns.size()) {
1912       return;
1913     }
1914     set_border_set_pattern(border_set_id, border_kind, patterns[i]);
1915   }
1916 }
1917 
1918 /**
1919  * @brief Returns whether a border set generates tiles inside or outside the selection.
1920  * @param border_set_id A border set id.
1921  * @return @c true if this is an inner border set.
1922  * @throws EditorException in case of error.
1923  */
is_border_set_inner(const QString & border_set_id) const1924 bool TilesetModel::is_border_set_inner(const QString& border_set_id) const {
1925 
1926   if (!border_set_exists(border_set_id)) {
1927     throw EditorException(tr("No such contour: '%1'").arg(border_set_id));
1928   }
1929 
1930   return tileset.get_border_set(border_set_id.toStdString()).is_inner();
1931 }
1932 
1933 /**
1934  * @brief Sets whether a border set generates tiles inside or outside the selection.
1935  *
1936  * Emits border_set_inner_changed() if there is a change.
1937  *
1938  * @param border_set_id A border set id.
1939  * @param inner @c true to make this border set an inner one.
1940  * @throws EditorException in case of error.
1941  */
set_border_set_inner(const QString & border_set_id,bool inner)1942 void TilesetModel::set_border_set_inner(const QString& border_set_id, bool inner) {
1943 
1944   if (!border_set_exists(border_set_id)) {
1945     throw EditorException(tr("No such contour: '%1'").arg(border_set_id));
1946   }
1947 
1948   if (inner == is_border_set_inner(border_set_id)) {
1949     // No change.
1950     return;
1951   }
1952 
1953   tileset.get_border_set(border_set_id.toStdString()).set_inner(inner);
1954 
1955   emit border_set_inner_changed(border_set_id, inner);
1956 }
1957 
1958 /**
1959  * @brief Returns whether a string is a valid border set id.
1960  * @param border_set_id The id to check.
1961  * @return @c true if this id is legal.
1962  */
is_valid_border_set_id(const QString & border_set_id)1963 bool TilesetModel::is_valid_border_set_id(const QString& border_set_id) {
1964 
1965   if (border_set_id.isEmpty()) {
1966       return false;
1967   }
1968 
1969   if (
1970       border_set_id.contains('\"') ||
1971       border_set_id.contains('\'') ||
1972       border_set_id.contains('\\') ||
1973       border_set_id.contains('\n') ||
1974       border_set_id.contains('\r')
1975   ) {
1976     return false;
1977   }
1978 
1979   return true;
1980 }
1981 
1982 /**
1983  * @brief Returns an image representing the specified border set.
1984  *
1985  * The image has the size of the pattern.
1986  *
1987  * @param border_set_id A border set id.
1988  * @return The corresponding image.
1989  * Returns a null pixmap if the tileset image is not loaded
1990  * or if there is no such border set.
1991  */
get_border_set_image(const QString & border_set_id) const1992 QPixmap TilesetModel::get_border_set_image(const QString& border_set_id) const {
1993 
1994   if (!border_set_exists(border_set_id)) {
1995     return QPixmap();
1996   }
1997 
1998   QString pattern_id = get_border_set_pattern(border_set_id, BorderKind::TOP);
1999   return get_pattern_image(id_to_index(pattern_id));
2000 }
2001 
2002 /**
2003  * @brief Returns a 32x32 icon representing the specified border set.
2004  * @param border_set_id A border set id.
2005  * @return The corresponding icon.
2006  * Returns a null pixmap if the tileset image is not loaded
2007  * or if there is no such border set.
2008  */
get_border_set_icon(const QString & border_set_id) const2009 QPixmap TilesetModel::get_border_set_icon(const QString& border_set_id) const {
2010 
2011   if (!border_set_exists(border_set_id)) {
2012     return QPixmap();
2013   }
2014 
2015   QString pattern_id = get_border_set_pattern(border_set_id, BorderKind::TOP);
2016   return get_pattern_icon(id_to_index(pattern_id));
2017 }
2018 
2019 }
2020