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/gui_tools.h"
18 #include "widgets/strings_editor.h"
19 #include "widgets/new_string_dialog.h"
20 #include "widgets/change_string_key_dialog.h"
21 #include "editor_exception.h"
22 #include "quest.h"
23 #include "strings_model.h"
24 #include <QUndoStack>
25 #include <QMessageBox>
26 #include <cmath>
27 
28 namespace SolarusEditor {
29 
30 namespace {
31 
32 /**
33  * @brief Parent class of all undoable commands of the strings editor.
34  */
35 class StringsEditorCommand : public QUndoCommand {
36 
37 public:
38 
StringsEditorCommand(StringsEditor & editor,const QString & text)39   StringsEditorCommand(StringsEditor& editor, const QString& text) :
40     QUndoCommand(text),
41     editor(editor) {
42   }
43 
get_editor() const44   StringsEditor& get_editor() const {
45     return editor;
46   }
47 
get_model() const48   StringsModel& get_model() const {
49     return editor.get_model();
50   }
51 
52 private:
53 
54   StringsEditor& editor;
55 
56 };
57 
58 /**
59  * @brief Create a string.
60  */
61 class CreateStringCommand : public StringsEditorCommand {
62 
63 public:
64 
CreateStringCommand(StringsEditor & editor,const QString & key,const QString & value="")65   CreateStringCommand(
66       StringsEditor& editor, const QString& key, const QString& value = "") :
67     StringsEditorCommand(editor, StringsEditor::tr("Create string")),
68     key(key),
69     value(value) {
70   }
71 
undo()72   virtual void undo() override {
73 
74     get_model().delete_string(key);
75   }
76 
redo()77   virtual void redo() override {
78 
79     get_model().create_string(key, value);
80     get_model().set_selected_key(key);
81   }
82 
83 private:
84 
85   QString key;
86   QString value;
87 };
88 
89 /**
90  * @brief Duplicate string(s).
91  */
92 class DuplicateStringsCommand : public StringsEditorCommand {
93 
94 public:
95 
DuplicateStringsCommand(StringsEditor & editor,const QString & prefix,const QString & new_prefix)96   DuplicateStringsCommand(
97       StringsEditor& editor, const QString& prefix, const QString& new_prefix) :
98     StringsEditorCommand(editor, StringsEditor::tr("Duplicate strings")),
99     prefix(prefix),
100     new_prefix(new_prefix) {
101   }
102 
undo()103   virtual void undo() override {
104 
105     get_model().delete_prefix(new_prefix);
106   }
107 
redo()108   virtual void redo() override {
109 
110     get_model().duplicate_strings(prefix, new_prefix);
111     get_model().set_selected_key(new_prefix);
112   }
113 
114 private:
115 
116   QString prefix;
117   QString new_prefix;
118 };
119 
120 /**
121  * @brief Change string key.
122  */
123 class SetStringKeyCommand : public StringsEditorCommand {
124 
125 public:
126 
SetStringKeyCommand(StringsEditor & editor,const QString & key,const QString & new_key)127   SetStringKeyCommand(
128       StringsEditor& editor, const QString& key, const QString& new_key) :
129     StringsEditorCommand(editor, StringsEditor::tr("Change string key")),
130     old_key(key),
131     new_key(new_key) {
132   }
133 
undo()134   virtual void undo() override {
135 
136     get_model().set_string_key(new_key, old_key);
137     get_model().set_selected_key(old_key);
138   }
139 
redo()140   virtual void redo() override {
141 
142     get_model().set_string_key(old_key, new_key);
143     get_model().set_selected_key(new_key);
144   }
145 
146 private:
147 
148   QString old_key;
149   QString new_key;
150 };
151 
152 /**
153  * @brief Change string key.
154  */
155 class SetKeyPrefixCommand : public StringsEditorCommand {
156 
157 public:
158 
SetKeyPrefixCommand(StringsEditor & editor,const QString & olf_prefix,const QString & new_prefix)159   SetKeyPrefixCommand(
160       StringsEditor& editor, const QString& olf_prefix,
161       const QString& new_prefix) :
162     StringsEditorCommand(editor, StringsEditor::tr("Change string key prefix")),
163     old_prefix(olf_prefix),
164     new_prefix(new_prefix) {
165   }
166 
undo()167   virtual void undo() override {
168 
169     for (const auto& pair : edited_keys) {
170       get_model().set_string_key(pair.second, pair.first);
171     }
172     if (!edited_keys.isEmpty()) {
173       get_model().set_selected_key(edited_keys.front().first);
174     }
175   }
176 
redo()177   virtual void redo() override {
178 
179     edited_keys = get_model().set_string_key_prefix(old_prefix, new_prefix);
180     if (!edited_keys.isEmpty()) {
181       get_model().set_selected_key(edited_keys.front().second);
182     }
183   }
184 
185 private:
186 
187   QString old_prefix;
188   QString new_prefix;
189   QList<QPair<QString, QString>> edited_keys;
190 };
191 
192 /**
193  * @brief Delete a string.
194  */
195 class DeleteStringCommand : public StringsEditorCommand {
196 
197 public:
198 
DeleteStringCommand(StringsEditor & editor,const QString & key)199   DeleteStringCommand(StringsEditor& editor, const QString& key) :
200     StringsEditorCommand(editor, StringsEditor::tr("Delete string")),
201     key(key),
202     value(get_model().get_string(key)) {
203   }
204 
undo()205   virtual void undo() override {
206 
207     get_model().create_string(key, value);
208     get_model().set_selected_key(key);
209   }
210 
redo()211   virtual void redo() override {
212 
213     get_model().delete_string(key);
214   }
215 
216 private:
217 
218   QString key;
219   QString value;
220 };
221 
222 /**
223  * @brief Delete several strings.
224  */
225 class DeleteStringsCommand : public StringsEditorCommand {
226 
227 public:
228 
DeleteStringsCommand(StringsEditor & editor,const QString & prefix)229   DeleteStringsCommand(StringsEditor& editor, const QString& prefix) :
230     StringsEditorCommand(editor, StringsEditor::tr("Delete strings")),
231     prefix(prefix) {
232   }
233 
undo()234   virtual void undo() override {
235 
236     for (const auto& pair : values) {
237       get_model().create_string(pair.first, pair.second);
238     }
239     if (!values.isEmpty()) {
240       get_model().set_selected_key(values.front().first);
241     }
242   }
243 
redo()244   virtual void redo() override {
245 
246     values = get_model().delete_prefix(prefix);
247   }
248 
249 private:
250 
251   QString prefix;
252   QList<QPair<QString, QString>> values;
253 };
254 
255 /**
256  * @brief Change string value.
257  */
258 class SetStringValueCommand : public StringsEditorCommand {
259 
260 public:
261 
SetStringValueCommand(StringsEditor & editor,const QString & key,const QString & value)262   SetStringValueCommand(
263       StringsEditor& editor, const QString& key, const QString& value) :
264     StringsEditorCommand(editor, StringsEditor::tr("Change string value")),
265     key(key),
266     old_value(get_model().get_string(key)),
267     new_value(value) {
268   }
269 
undo()270   virtual void undo() override {
271 
272     get_model().set_string(key, old_value);
273     get_model().set_selected_key(key);
274   }
275 
redo()276   virtual void redo() override {
277 
278     get_model().set_string(key, new_value);
279     get_model().set_selected_key(key);
280   }
281 
282 private:
283 
284   QString key;
285   QString old_value;
286   QString new_value;
287 };
288 
289 }
290 
291 /**
292  * @brief Creates a strings editor.
293  * @param quest The quest containing the file.
294  * @param language_id Language id of the strings data file to open.
295  * @param parent The parent object or nullptr.
296  * @throws EditorException If the file could not be opened.
297  */
StringsEditor(Quest & quest,const QString & language_id,QWidget * parent)298 StringsEditor::StringsEditor(
299     Quest& quest, const QString& language_id, QWidget* parent) :
300   Editor(quest, quest.get_strings_path(language_id), parent),
301   language_id(language_id),
302   model(nullptr),
303   quest(quest) {
304 
305   ui.setupUi(this);
306 
307   // Open the file.
308   model = new StringsModel(quest, language_id, this);
309   get_undo_stack().setClean();
310 
311   // Editor properties.
312   set_title(tr("Strings %1").arg(language_id));
313   set_icon(QIcon(":/images/icon_strings.png"));
314   set_close_confirm_message(
315         tr("Strings '%1' have been modified. Save changes?").arg(language_id));
316 
317   // Prepare the gui.
318   ui.strings_tree_view->set_model(model);
319   ui.strings_tree_view->setColumnWidth(0, 300);
320   ui.strings_tree_view->setColumnHidden(2, true);
321 
322   ui.translation_field->set_resource_type(ResourceType::LANGUAGE);
323   ui.translation_field->set_quest(quest);
324   ui.translation_field->remove_id(language_id);
325   ui.translation_field->add_special_value("", tr("<No language>"), 0);
326   ui.translation_field->set_selected_id("");
327 
328   update();
329 
330   // Make connections.
331   connect(&get_database(),
332           SIGNAL(element_description_changed(ResourceType, const QString&, const QString&)),
333           this, SLOT(update_description_to_gui()));
334   connect(ui.description_field, SIGNAL(editingFinished()),
335           this, SLOT(set_description_from_gui()));
336 
337   connect(ui.create_button, SIGNAL(clicked()),
338           this, SLOT(create_string_requested()));
339   connect(ui.strings_tree_view, SIGNAL(create_string_requested()),
340           this, SLOT(create_string_requested()));
341 
342   connect(ui.duplicate_button, SIGNAL(clicked()),
343           this, SLOT(duplicate_string_requested()));
344   connect(ui.strings_tree_view, SIGNAL(duplicate_string_requested()),
345           this, SLOT(duplicate_string_requested()));
346 
347   connect(ui.set_key_button, SIGNAL(clicked()),
348           this, SLOT(change_string_key_requested()));
349   connect(ui.strings_tree_view, SIGNAL(set_string_key_requested()),
350           this, SLOT(change_string_key_requested()));
351 
352   connect(ui.delete_button, SIGNAL(clicked()),
353           this, SLOT(delete_string_requested()));
354   connect(ui.strings_tree_view, SIGNAL(delete_string_requested()),
355           this, SLOT(delete_string_requested()));
356 
357   connect(&model->get_selection_model(),
358           SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
359           this, SLOT(update_selection()));
360 
361   connect(model, SIGNAL(set_value_requested(QString,QString)),
362           this, SLOT(set_value_requested(QString,QString)));
363 
364   connect(ui.translation_field, SIGNAL(activated(QString)),
365           this, SLOT(translation_selector_activated()));
366   connect(ui.translation_refresh_button, SIGNAL(clicked()),
367           this, SLOT(translation_refresh_requested()));
368 }
369 
370 /**
371  * @brief Destructor.
372  */
~StringsEditor()373 StringsEditor::~StringsEditor() {
374   if (model != nullptr) {
375     delete model;
376   }
377 }
378 
379 /**
380  * @brief Returns the strings model being edited.
381  * @return The strings model.
382  */
get_model()383 StringsModel& StringsEditor::get_model() {
384   return *model;
385 }
386 
387 /**
388  * @copydoc Editor::save
389  */
save()390 void StringsEditor::save() {
391 
392   model->save();
393 }
394 
395 /**
396  * @brief Updates everything in the gui.
397  */
update()398 void StringsEditor::update() {
399 
400   update_language_id_field();
401   update_description_to_gui();
402   update_selection();
403 }
404 
405 /**
406  * @brief Updates the language id displaying.
407  */
update_language_id_field()408 void StringsEditor::update_language_id_field() {
409 
410   ui.language_id_field->setText(language_id);
411 }
412 
413 /**
414  * @brief Updates the content of the language description text edit.
415  */
update_description_to_gui()416 void StringsEditor::update_description_to_gui() {
417 
418   QString description = get_database().get_description(
419         ResourceType::LANGUAGE, language_id);
420   if (ui.description_field->text() != description) {
421     ui.description_field->setText(description);
422   }
423 }
424 
425 /**
426  * @brief Modifies the language description in the quest resource list with
427  * the new text entered by the user.
428  *
429  * If the new description is invalid, an error dialog is shown.
430  */
set_description_from_gui()431 void StringsEditor::set_description_from_gui() {
432 
433   QString description = ui.description_field->text();
434   if (description == get_database().get_description(
435         ResourceType::LANGUAGE, language_id)) {
436     return;
437   }
438 
439   if (description.isEmpty()) {
440     GuiTools::error_dialog(tr("Invalid description"));
441     update_description_to_gui();
442     return;
443   }
444 
445   const bool was_blocked = blockSignals(true);
446   try {
447     get_database().set_description(
448           ResourceType::LANGUAGE, language_id, description);
449     get_database().save();
450   }
451   catch (const EditorException& ex) {
452     ex.print_message();
453   }
454   update_description_to_gui();
455   blockSignals(was_blocked);
456 }
457 
458 /**
459  * @brief Updates the selection.
460  */
update_selection()461 void StringsEditor::update_selection() {
462 
463   // Ensures that the selected item is visible in the tree view
464   QString key = model->get_selected_key();
465   if (!key.isEmpty()) {
466     ui.strings_tree_view->scrollTo(model->key_to_index(key));
467   }
468 
469   // Update buttons
470   bool enable = !key.isEmpty() && model->prefix_exists(key);
471   ui.set_key_button->setEnabled(enable);
472   ui.duplicate_button->setEnabled(enable);
473   ui.delete_button->setEnabled(enable);
474 }
475 
476 /**
477  * @brief Slot called when the user wants to create a string.
478  */
create_string_requested()479 void StringsEditor::create_string_requested() {
480 
481   NewStringDialog dialog(model, model->get_selected_key(), "", this);
482 
483   int result = dialog.exec();
484   if (result != QDialog::Accepted) {
485     return;
486   }
487 
488   QString key = dialog.get_string_key();
489   QString value = dialog.get_string_value();
490   try_command(new CreateStringCommand(*this, key, value));
491 }
492 
493 /**
494  * @brief Slot called when the user wants to duplicate string(s).
495  */
duplicate_string_requested()496 void StringsEditor::duplicate_string_requested() {
497 
498   QString prefix = model->get_selected_key();
499   QString new_prefix = prefix + tr("_copy");
500   QString key;
501   if (!model->can_duplicate_strings(prefix, new_prefix, key)) {
502     GuiTools::error_dialog(tr("String '%1' already exists").arg(key));
503     return;
504   }
505 
506   try_command(new DuplicateStringsCommand(*this, prefix, new_prefix));
507 }
508 
509 /**
510  * @brief Slot called when the user wants to change the key of a string.
511  */
change_string_key_requested()512 void StringsEditor::change_string_key_requested() {
513 
514   if (model->is_selection_empty()) {
515     return;
516   }
517 
518   QString old_key = model->get_selected_key();
519   QStringList prefix_keys = model->get_keys(old_key);
520   bool exists = false;
521   bool is_prefix = false;
522 
523   if (prefix_keys.size() > 0) {
524     if (model->string_exists(old_key)) {
525       exists = true;
526       is_prefix = prefix_keys.size() > 1;
527     } else {
528       is_prefix = true;
529     }
530   } else {
531     return;
532   }
533 
534   ChangeStringKeyDialog dialog(
535         model, old_key, is_prefix, is_prefix && exists, this);
536 
537   int result = dialog.exec();
538   if (result != QDialog::Accepted) {
539     return;
540   }
541 
542   QString new_key = dialog.get_string_key();
543 
544   if (new_key == old_key) {
545     return;
546   }
547 
548   if (dialog.get_prefix()) {
549     try_command(new SetKeyPrefixCommand(*this, old_key, new_key));
550   } else {
551     try_command(new SetStringKeyCommand(*this, old_key, new_key));
552   }
553 }
554 
555 /**
556  * @brief Slot called when the user wants to delete a string.
557  */
delete_string_requested()558 void StringsEditor::delete_string_requested() {
559 
560   if (model->is_selection_empty()) {
561     return;
562   }
563 
564   QString key = model->get_selected_key();
565   if (!model->prefix_exists(key)) {
566     return;
567   }
568 
569   if (model->string_exists(key)) {
570     try_command(new DeleteStringCommand(*this, key));
571     return;
572   }
573 
574   QMessageBox::StandardButton answer = QMessageBox::question(
575         this,
576         tr("Delete confirmation"),
577         tr("Do you really want to delete all strings prefixed by '%1'?").arg(key),
578         QMessageBox::Yes | QMessageBox::No);
579 
580   if (answer != QMessageBox::Yes) {
581     return;
582   }
583 
584   try_command(new DeleteStringsCommand(*this, key));
585 }
586 
587 /**
588  * @brief Slot called when the user wants to set the value of a string.
589  * @param key The key of the string to edit.
590  * @param value The value to set.
591  */
set_value_requested(const QString & key,const QString & value)592 void StringsEditor::set_value_requested(
593     const QString& key, const QString& value) {
594 
595   // If no exists, try to create.
596   if (!model->string_exists(key)) {
597     if (!value.isEmpty()) {
598       try_command(new CreateStringCommand(*this, key, value));
599     }
600   }
601   // Else,
602   else {
603     // If value is empty, try to remove.
604     if (value.isEmpty()) {
605       try_command(new DeleteStringCommand(*this, key));
606     }
607     // Else if the value is different, try to change.
608     else if (value != model->get_string(key)) {
609       try_command(new SetStringValueCommand(*this, key, value));
610     }
611   }
612 }
613 
614 /**
615  * @brief Slot called when the user changes the language in the selector.
616  */
translation_selector_activated()617 void StringsEditor::translation_selector_activated() {
618 
619   const QString& old_language_id = model->get_translation_id();
620   const QString& new_language_id = ui.translation_field->get_selected_id();
621   if (new_language_id == old_language_id) {
622     // No change.
623     return;
624   }
625 
626   // If language id is empty.
627   if (new_language_id.isEmpty()) {
628     // Clear the translation and hide his column, return.
629     model->clear_translation();
630     ui.strings_tree_view->setColumnHidden(2, true);
631     ui.translation_refresh_button->setEnabled(false);
632     return;
633   }
634 
635 
636   // If the translation column is hide.
637   if (ui.strings_tree_view->isColumnHidden(2)) {
638     // Show the column.
639     ui.strings_tree_view->setColumnHidden(2, false);
640 
641     // Resize value and translation columns.
642     int col_width = ui.strings_tree_view->columnWidth(0);
643     int width = ui.strings_tree_view->viewport()->width() - col_width;
644     col_width = std::floor(width / 2.0);
645     ui.strings_tree_view->setColumnWidth(1, col_width);
646     ui.strings_tree_view->setColumnWidth(2, col_width);
647   }
648 
649   // Set the translation.
650   model->set_translation_id(new_language_id);
651   ui.translation_refresh_button->setEnabled(true);
652 }
653 
654 /**
655  * @brief Slot called when the user wants to refresh the translation language.
656  */
translation_refresh_requested()657 void StringsEditor::translation_refresh_requested() {
658   if (ui.translation_field->get_selected_id().isEmpty()) {
659     return;
660   }
661   model->reload_translation();
662 }
663 
664 }
665