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