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