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 "widgets/resource_model.h"
18 #include "editor_exception.h"
19 #include "quest.h"
20 #include "quest_database.h"
21 #include "sprite_model.h"
22
23 namespace SolarusEditor {
24
25 /**
26 * @brief Creates a resource model.
27 * @param quest The quest.
28 * @param resource_type Type of resources to show.
29 * @param parent The parent object or nullptr.
30 */
ResourceModel(const Quest & quest,ResourceType resource_type,QObject * parent)31 ResourceModel::ResourceModel(const Quest& quest, ResourceType resource_type, QObject* parent) :
32 QStandardItemModel(parent),
33 quest(quest),
34 resource_type(resource_type),
35 items(),
36 icons(),
37 directory_icon(":/images/icon_folder_open.png"),
38 tileset_id() {
39
40 const QuestDatabase& database = get_database();
41
42 const QStringList& ids = database.get_elements(this->resource_type);
43 for (const QString& id : ids) {
44 add_element(id);
45 }
46
47 connect(&database, SIGNAL(element_added(ResourceType, QString, QString)),
48 this, SLOT(element_added(ResourceType, QString, QString)));
49 connect(&database, SIGNAL(element_removed(ResourceType, QString)),
50 this, SLOT(element_removed(ResourceType, QString)));
51 connect(&database, SIGNAL(element_renamed(ResourceType, QString, QString)),
52 this, SLOT(element_renamed(ResourceType, QString, QString)));
53 connect(&database, SIGNAL(element_description_changed(ResourceType, QString, QString)),
54 this, SLOT(element_description_changed(ResourceType, QString, QString)));
55 }
56
57 /**
58 * @brief Returns the quest.
59 * @return The quest.
60 */
get_quest() const61 const Quest& ResourceModel::get_quest() const {
62 return quest;
63 }
64
65 /**
66 * @brief Returns the resources and files of the quest.
67 * @return The quest database.
68 */
get_database() const69 const QuestDatabase& ResourceModel::get_database() const {
70 return quest.get_database();
71 }
72
73 /**
74 * @brief Returns the id of the current tileset.
75 *
76 * This tileset is used for icons of tileset-dependent sprites.
77 *
78 * @return The current tileset id or an empty string.
79 */
get_tileset_id() const80 QString ResourceModel::get_tileset_id() const {
81 return tileset_id;
82 }
83
84 /**
85 * @brief Sets the id of the current tileset.
86 *
87 * This tileset is used for icons of tileset-dependent sprites.
88 *
89 * @param tileset_id The current tileset id or an empty string to unset it.
90 */
set_tileset_id(const QString & tileset_id)91 void ResourceModel::set_tileset_id(const QString& tileset_id) {
92
93 this->tileset_id = tileset_id;
94
95 if (resource_type == ResourceType::SPRITE ||
96 resource_type == ResourceType::ENEMY ||
97 resource_type == ResourceType::ITEM) {
98
99 // Icons may change.
100 icons.clear(); // Clear the icon cache.
101 QVector<int> roles;
102 roles << Qt::DecorationRole;
103 dataChanged(QModelIndex(), QModelIndex(), roles);
104 }
105 }
106
107 /**
108 * @brief Returns the index of the specified element.
109 * @param element_id Id of a resource element.
110 * @return The corresponding index in this model
111 * or an invalid index if it does not exist.
112 */
get_element_index(const QString & element_id) const113 QModelIndex ResourceModel::get_element_index(const QString& element_id) const {
114
115 const QStandardItem* item = get_element_item(element_id);
116 if (item == nullptr) {
117 return QModelIndex();
118 }
119 return item->index();
120 }
121
122 /**
123 * @brief Adds an item for a special value that is not an existing resource element.
124 *
125 * You can use this function to add a fake item like "None" or "Unchanged".
126 *
127 * @param id String to identify the item, replacing the resource element id.
128 * @param text Text to show in the combo box for this item.
129 * @param index Index where to insert the item.
130 */
add_special_value(const QString & id,const QString & text,int index)131 void ResourceModel::add_special_value(
132 const QString& id, const QString& text, int index) {
133
134 QStandardItem* item = new QStandardItem(text);
135 item->setData(id, Qt::UserRole);
136 items.insert(id, item);
137 insertRow(index, item);
138 }
139
140 /**
141 * @brief Removes from the model the item with the specified id.
142 * @param id Id of the resource to remove.
143 */
remove_id(const QString & id)144 void ResourceModel::remove_id(const QString& id) {
145
146 const QStandardItem* item = get_element_item(id);
147 if (item == nullptr) {
148 return;
149 }
150 remove_element(id);
151 }
152
153 /**
154 * @brief Adds to the model an item for the specified resource element.
155 * @param element_id Id of the resource element to add.
156 */
add_element(const QString & element_id)157 void ResourceModel::add_element(const QString& element_id) {
158
159 QStringList files = element_id.split('/', QString::SkipEmptyParts);
160 QStandardItem* parent = invisibleRootItem();
161 while (files.size() > 1) {
162 parent = find_or_create_dir_item(*parent, files.first());
163 files.removeFirst();
164 }
165
166 QStandardItem* item = create_element_item(element_id);
167 parent->appendRow(item); // TODO insert at the correct position
168 }
169
170 /**
171 * @brief Removes from the model the item of the specified resource element.
172 * @param element_id Id of the resource element to remove.
173 */
remove_element(const QString & element_id)174 void ResourceModel::remove_element(const QString& element_id) {
175
176 const QModelIndex& index = get_element_index(element_id);
177 if (!index.isValid()) {
178 return;
179 }
180
181 removeRow(index.row(), index.parent());
182 items.remove(element_id);
183 }
184
185 /**
186 * @brief Creates a new leaf item with the specified element id.
187 * @param element_id Id of the element to create.
188 * @return The created item.
189 */
create_element_item(const QString & element_id)190 QStandardItem* ResourceModel::create_element_item(const QString& element_id) {
191
192 QString description = get_database().get_description(
193 resource_type, element_id);
194
195 QString text = description;
196 if (text.isEmpty()) {
197 // No description available: fallback to the last part of the element id.
198 text = element_id.section('/', -1, -1);
199 }
200 QStandardItem* item = new QStandardItem(text);
201 item->setData(element_id, Qt::UserRole);
202 items.insert(element_id, item);
203 return item;
204 }
205
206 /**
207 * @brief Returns the leaf item with the specified element id.
208 * @param element_id Id of the element to get.
209 * @return The item or nullptr if it does not exist.
210 */
get_element_item(const QString & element_id) const211 const QStandardItem* ResourceModel::get_element_item(const QString& element_id) const {
212
213 const auto& it = items.find(element_id);
214 if (it == items.end()) {
215 return nullptr;
216 }
217
218 return it.value();
219 }
220
221 /**
222 * @brief Returns the leaf item with the specified element id.
223 *
224 * Non-const version.
225 *
226 * @param element_id Id of the element to get.
227 * @return The item or nullptr if it does not exist.
228 */
get_element_item(const QString & element_id)229 QStandardItem* ResourceModel::get_element_item(const QString& element_id) {
230
231 const auto& it = items.find(element_id);
232 if (it == items.end()) {
233 return nullptr;
234 }
235
236 return it.value();
237 }
238
239 /**
240 * @brief Returns the item with the specified directory name.
241 * @param parent The parent item.
242 * @param dir_name Name of the subdirectory to get.
243 * @return The child. It is created if it does not exist yet.
244 */
find_or_create_dir_item(QStandardItem & parent,const QString & dir_name)245 QStandardItem* ResourceModel::find_or_create_dir_item(
246 QStandardItem& parent, const QString& dir_name) {
247
248 for (int i = 0; i < parent.rowCount(); ++i) {
249 QStandardItem* child = parent.child(i, 0);
250 QString name = child->data(Qt::DisplayRole).toString();
251 if (name == dir_name) {
252 return child;
253 }
254
255 if (name > dir_name) {
256 child = create_dir_item(dir_name);
257 parent.insertRow(i, child);
258 return child;
259 }
260 }
261
262 QStandardItem* child = create_dir_item(dir_name);
263 parent.appendRow(child);
264 return child;
265 }
266
267 /**
268 * @brief Creates a new item with the specified directory name.
269 * @param dir_name Name of a directory.
270 * @return The created item.
271 */
create_dir_item(const QString & dir_name)272 QStandardItem* ResourceModel::create_dir_item(const QString& dir_name) {
273
274 QStandardItem* item = new QStandardItem(dir_name);
275 item->setSelectable(false);
276 return item;
277 }
278
279 /**
280 * @brief Returns an icon for the given element.
281 * @param element_id Id of a resource element.
282 * @return An appropriate icon.
283 */
create_icon(const QString & element_id) const284 QIcon ResourceModel::create_icon(const QString& element_id) const {
285
286 const Quest& quest = get_quest();
287 Q_ASSERT(!element_id.isEmpty());
288 Q_ASSERT(quest.get_database().exists(resource_type, element_id));
289
290 try {
291 if (resource_type == ResourceType::SPRITE) {
292 // Special case of sprites: show the sprite icon.
293 if (quest.exists(quest.get_sprite_path(element_id))) {
294 SpriteModel sprite(quest, element_id);
295 sprite.set_tileset_id(tileset_id);
296 const QPixmap& pixmap = sprite.get_icon();
297 if (!pixmap.isNull()) {
298 return QIcon(pixmap);
299 }
300 }
301 }
302 else if (resource_type == ResourceType::ENEMY) {
303 // Enemy: show the enemy's sprite.
304 QString sprite_id = QString("enemies/%1").arg(element_id);
305 SpriteModel sprite(quest, sprite_id);
306 sprite.set_tileset_id(tileset_id);
307 const QPixmap& pixmap = sprite.get_icon();
308 if (!pixmap.isNull()) {
309 return QIcon(pixmap);
310 }
311 }
312 else if (resource_type == ResourceType::ITEM) {
313 // Item: show the treasure's sprite.
314 QString sprite_id = "entities/items";
315 SpriteModel sprite(quest, sprite_id);
316 sprite.set_tileset_id(tileset_id);
317 const QPixmap& pixmap = sprite.get_direction_icon({ element_id, 0 });
318 if (!pixmap.isNull()) {
319 return QIcon(pixmap);
320 }
321 }
322 }
323 catch (const EditorException&) {
324 // The sprite is missing: just return the generic
325 // icon instead.
326 }
327
328 // Return an icon representing the resource type.
329 QString resource_type_name = quest.get_database().get_lua_name(resource_type);
330 return QIcon(":/images/icon_resource_" + resource_type_name + ".png");
331 }
332
333 /**
334 * @brief Slot called when a resource element is added to the quest.
335 * @param type A type of resource.
336 * @param id Id of the element added.
337 * @param description Description of the element added.
338 */
element_added(ResourceType type,const QString & id,const QString &)339 void ResourceModel::element_added(
340 ResourceType type, const QString& id, const QString& /* description */) {
341
342 if (type != this->resource_type) {
343 return;
344 }
345
346 // Add an item to the model.
347 add_element(id);
348 }
349
350 /**
351 * @brief Slot called when a resource element is removed from the quest.
352 * @param type A type of resource.
353 * @param id Id of the element removed.
354 */
element_removed(ResourceType type,const QString & id)355 void ResourceModel::element_removed(
356 ResourceType type, const QString& id) {
357
358 if (type != this->resource_type) {
359 return;
360 }
361
362 // Remove the item from the model.
363 remove_element(id);
364 }
365
366 /**
367 * @brief Slot called when a resource element is renamed.
368 * @param type A type of resource.
369 * @param old_id Id of the element before rename.
370 * @param new_id Id of the element after rename.
371 */
element_renamed(ResourceType type,const QString & old_id,const QString & new_id)372 void ResourceModel::element_renamed(
373 ResourceType type, const QString& old_id, const QString& new_id) {
374
375 if (type != this->resource_type) {
376 return;
377 }
378
379 QStandardItem* item = get_element_item(old_id);
380 if (item == nullptr) {
381 // Item not found, maybe it has been removed dynamicaly
382 // (e.g. in StringsEditor).
383 return;
384 }
385
386 item->setData(new_id, Qt::UserRole);
387
388 items.remove(old_id);
389 items.insert(new_id, item);
390 // TODO update the order
391 }
392
393 /**
394 * @brief Slot called when the description of a resource element changes.
395 * @param type A type of resource.
396 * @param id Id of the element.
397 * @param new_description The new description.
398 */
element_description_changed(ResourceType type,const QString & id,const QString & new_description)399 void ResourceModel::element_description_changed(
400 ResourceType type, const QString& id, const QString& new_description) {
401
402 if (type != this->resource_type) {
403 return;
404 }
405
406 QStandardItem* item = get_element_item(id);
407 if (item == nullptr) {
408 // Item not found, maybe it has been removed dynamically
409 // (e.g. in StringsEditor).
410 return;
411 }
412
413 item->setData(new_description, Qt::DisplayRole);
414 }
415
416 /**
417 * @brief Returns the data of an item.
418 *
419 * Reimplemented from QStandardItemModel to create sprite icons lazily.
420 *
421 * @param index Index of the item to get.
422 * @param role The wanted role.
423 * @return The corresponding data.
424 */
data(const QModelIndex & index,int role) const425 QVariant ResourceModel::data(const QModelIndex& index, int role) const {
426
427 // Don't use QStandardItemModel storage features for icons.
428 // Load them on-demand instead.
429 if (role == Qt::DecorationRole) {
430
431 const QStandardItem* item = itemFromIndex(index);
432 if (item == nullptr) {
433 // Invalid index;
434 return QVariant();
435 }
436
437 const QString& element_id = item->data(Qt::UserRole).toString();
438 if (element_id.isEmpty()) {
439 // Not a resource element: maybe a directory.
440 if (rowCount(index) > 0) {
441 return directory_icon; // Directory item.
442 }
443 return QIcon();
444 }
445
446 if (!get_database().exists(resource_type, element_id)) {
447 // Special item.
448 return QIcon();
449 }
450
451 // Resource element item.
452 Q_ASSERT(!element_id.isEmpty());
453 auto it = icons.find(element_id);
454 if (it != icons.end()) {
455 // Icon already loaded.
456 return it.value();
457 }
458 else {
459 // Icon not loaded yet.
460 QIcon icon = create_icon(element_id);
461 icons.insert(element_id, icon);
462 return icon;
463 }
464 }
465
466 return QStandardItemModel::data(index, role);
467 }
468
469 }
470